diff --git a/.github/instructions/ai.instructions.md b/.github/instructions/ai.instructions.md index 142b1803..820e270c 100644 --- a/.github/instructions/ai.instructions.md +++ b/.github/instructions/ai.instructions.md @@ -294,8 +294,8 @@ the Wizard's `Custom` path. per-command Form permissions (`selectPermission`, `insertPermission`, `updatePermission`, `deletePermission`). In `Otomatik` mode the base is the read permission the Wizard created for the component and the commands get `''` / `.Create` / `.Update` / `.Delete` suffixes. -- Manager screen permissions: `App.DeveloperKit.Components{,.Create,.Update,.Delete}`. -- Every save/delete writes `configs/seeds/{scope}/custom/{Name}.json`; the endpoints it uses are +- List screen permissions: `App.DeveloperKit.CustomComponents{,.Create,.Update,.Delete}`. +- Every save/delete is mirrored into `configs/seeds/{scope}/data/App.DeveloperKit.CustomComponents.json`; the endpoints it uses are referenced through `crud/{Entity}.json`. A component that is not seed-backed is not done. ### 10.2 SQL View Designer @@ -655,15 +655,16 @@ a deploy. AI must learn and teach implementation flow primarily from these asset - PermissionsData.json - HostData.json - LanguagesData.json -- WizardDataSeeder.cs / CustomComponentDataSeeder.cs / CrudDataSeeder.cs — the seeders that read +- WizardDataSeeder.cs / CrudDataSeeder.cs — the seeders that read surface B below **B. Runtime seeds (written by the running application)** — `configs/seeds/`, resolved through -`SeedPathResolver`. The Wizard, Component Manager and CRUD Endpoint Manager write here; the +`SeedPathResolver`. The Wizard, the Custom Components screen / Visual Designer and the CRUD +Endpoint Manager write here; the seeders above restore them when the database is recreated. Scope folders mirror the CDN layout: ``` -configs/seeds/host/{wizard,custom,crud,sql/{object,execute},postgres/{object,execute}} +configs/seeds/host/{wizard,data,crud,sql/{object,execute},postgres/{object,execute}} configs/seeds/tenants/{tenantId}/… same layout, tenant scope configs/seeds/.imports/{importId}/… wizard import staging + backup (not scanned) ``` @@ -891,7 +892,8 @@ Exporting a wizard produces a zip whose paths are relative to the scope folder (`host` / `tenants/{tenantId}`) and which carries every dependency: - `wizard/{file}.json` -- `custom/{component}.json` (with its dependencies) — `Custom` path +- `data/App.DeveloperKit.CustomComponents.json` — `Custom` path; the file is filtered down to the + exported component and its dependency chain, never exported whole - `crud/{entity}.json` — endpoints those components use - `{sql|postgres}/{object|execute}/{object}.sql` — the object behind `SelectCommand` on the `List` path @@ -899,9 +901,29 @@ Exporting a wizard produces a zip whose paths are relative to the scope folder Import is two-phase: `AnalyzeImport` (unzip to `.imports/{id}/staged`, classify each entry as `New` / `Identical` / `Conflict`, conflicts resolved in a diff editor) then `ApplyImport` (file-by-file write, previous content backed up so `RollbackImport` can undo the whole batch), -closed by `CompleteImport`. Limits: allowed root folders `wizard`, `crud`, `custom`, `sql`, -`postgres` (and only `object` / `execute` under the SQL provider folders); 5 MB per entry, 50 MB -per archive, 500 entries. Permissions: `App.Listforms.Wizard.Export` / `.Import`. +closed by `CompleteImport`. + +A `data/*.json` entry is compared and merged **by row, not by file**: the diff shows only the +rows whose keys the archive carries (file-level `GeneratedAt`/`Order` are excluded), so unrelated +records never surface as a difference — no matching row means `New` (insert), a matching identical +row means `Identical` (nothing is transferred), a matching different row means `Conflict` (update). +The file itself is never overwritten: rows are merged into the target by key (incoming row wins, +target-only rows are kept). `CompleteImport` then applies exactly the +`data/` files it wrote to the database through `IListFormSeedDataApplier` with +`overwriteExisting: true` — a missing row is inserted, an existing one is updated from the file +(unlike migrate/seed, where a possibly stale file must not overwrite live data), and a row that was +soft deleted is revived by clearing the columns the screen's `DeleteCommand` sets +(`IsDeleted`/`DeletionTime`/`DeleterId`), otherwise the default filter would keep hiding it — and +reports the inserted/updated/skipped counts. Data files that were already identical to the target +are applied too: there was nothing to write, but the rows may be missing or soft deleted in the +database. A file the user chose to keep (different, not written) is left alone. This step is not +covered by `RollbackImport`, which only restores files and must be run before it. The other +folders (`wizard`, `crud`, `sql`) stay as files and reach the database through the seeders or a +wizard deploy. + +Limits: allowed root folders `wizard`, `crud`, `data`, `sql`, `postgres` (and only `object` / +`execute` under the SQL provider folders); 5 MB per entry, 50 MB per archive, 500 entries. +Permissions: `App.Listforms.Wizard.Export` / `.Import`. AI must not propose moving a screen between environments by copying a single json file. diff --git a/.github/instructions/list.instructions.md b/.github/instructions/list.instructions.md index e07ef219..81affdad 100644 --- a/.github/instructions/list.instructions.md +++ b/.github/instructions/list.instructions.md @@ -8,7 +8,7 @@ This document summarizes the rules, standards, and step-by-step instructions for > **Bu doküman depo içi seeder'larla (DbMigrator/Migrations) modül eklemeyi anlatır.** -> Çalışma zamanında Wizard / Component Manager / CRUD Endpoint Manager ile üretim yapacaksan +> Çalışma zamanında Wizard / Custom Components / CRUD Endpoint Manager ile üretim yapacaksan > `lowcode.instructions.md` esastır; oradaki seed dosyaları `configs/seeds` altına düşer. ## General Principles diff --git a/.github/instructions/lowcode-reference.instructions.md b/.github/instructions/lowcode-reference.instructions.md index f6711706..934c4260 100644 --- a/.github/instructions/lowcode-reference.instructions.md +++ b/.github/instructions/lowcode-reference.instructions.md @@ -131,7 +131,7 @@ ve çoğu isteğin cevabı aslında oradadır: | `CustomJsSourcesJson` · `CustomStyleSourcesJson` | Sayfaya enjekte edilen JS/CSS | §5.5 | | `AdaptiveLayoutJson` | Dar ekranda öğe gizleme (grafik) | §8.4 | | `CommonJson` · `DataSourceJson` | Grafik geneli ve grafik sorgusu | §8.2 · §8.3 | -| `Width` · `Height` · `FullHeight` | Ekran boyutu | — | +| `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 | @@ -531,6 +531,9 @@ ListForm editöründeki tipli formdan düzenlenemez; kalıcı kullanılacaksa C# `PriorityExpr`, `CompletedExpr`, `OrderExpr`, `TimesheetExpr`, `SubTaskExpr`, `StatusOrder` (kolon sırası, virgüllü), `AllowDragging` (varsayılan `true`). +Panonun yüksekliği `TodoOptionJson` ile değil ekranın `Height` kolonuyla belirlenir; boş +bırakıldığında pano sayfanın kalan yüksekliğini kaplar ve her kolon kendi içinde kayar. + > `*` işaretli alan boşsa seeder o görünümün JSON'unu **yazmaz**; bayrağı açmak yetmez. --- @@ -1183,7 +1186,7 @@ soneklerini alır. | **Developer Kit › SQL Query Manager** | `App.SqlQueryManager` · `/admin/sqlQueryManager` | — | — | View/procedure/function yazma ve çalıştırma; karar sırasının 2. adımı buradan başlar. | | **Developer Kit › Custom Endpoints** | `App.DeveloperKit.CustomEndpoints` | `Sas_H_CustomEndpoint` | tenant | `Name`, `Url`, `Method`, `DataSourceCode`, `Sql`, `ParametersJson`, `PermissionsJson` — parametreli SQL'i yetkili bir HTTP ucuna çevirir. | | **Developer Kit › Dynamic Services** | `App.DeveloperKit.DynamicServices` · `/admin/list/App.DeveloperKit.DynamicServices` | `Sas_H_DynamicService` | host | Liste ListForm ekranıdır; kod, listenin toolbar/satır butonlarından açılan `DynamicServiceEditor` diyalogunda (Monaco) yazılıp derlenir ve yayınlanır. SQL'in yetmediği iş kuralı için 3. adım. | -| **Developer Kit › Components** | `App.DeveloperKit.Components` · `/admin/developerkit/components` | `Sas_H_CustomComponent` | host | Custom component listesi ve **Visual Designer** girişi (§10). | +| **Developer Kit › Components** | `App.DeveloperKit.CustomComponents` · `/admin/list/App.DeveloperKit.CustomComponents` | `Sas_H_CustomComponent` | host | Custom component listesi (ListForm) ve satırdaki **Design** butonuyla **Visual Designer** girişi (§10). Kayıtlar `data/App.DeveloperKit.CustomComponents.json` dosyasına senkronlanır. | | **Developer Kit › ListForm** | `App.Listforms.Listform` | `Sas_H_ListForm` · `Sas_H_ListFormField` · `Sas_H_ListFormWorkflow` · `Sas_H_ListFormCustomization` · `Sas_H_ListFormImport` | host | Ekran tanımlarının kendisi. Bu dosyada anlatılan bütün JSON kolonlarının canlı hâli; düzenleyicisi `/admin/listform/edit/{kod}`. | | **Developer Kit › Wizard Manager** | menü kodu `App.Listforms.WizardManager` · yetki `App.Listforms.Wizard` · `/admin/listform/wizardManager` | — | host | Wizard seed dosyalarının listesi, düzenlemesi, silinmesi, export/import'u. Sihirbazın kendisi ayrı bir rotadadır: `/admin/listform/wizard`. | | **Forum Management** | `App.ForumManagement` (grup menüsü) | `Sas_T_ForumCategory` · `Sas_T_ForumTopic` · `Sas_T_ForumPost` | tenant | Altında Categories / Topics / Posts ListForm ekranları; yayın tarafı Administration'daki `/admin/forum`. | @@ -1272,7 +1275,7 @@ görünürlük `checkPermission` ile bileşen içinde kurulur (`lowcode` §7.5). | `/admin/listform/wizardManager` | `listForm/wizard/WizardFileManager` | `App.Listforms.Wizard` | | `/admin/listform/edit/:listFormCode` | `listForm/edit/FormEdit` | `[]` — yetki ListForm tarafında | | `/admin/sqlQueryManager` | `developerKit/SqlQueryManager` | `App.SqlQueryManager` | -| `/admin/developerkit/components{,/edit/:id}` | `ComponentManagerPage` · `ComponentCodeLayout` | `App.DeveloperKit.Components` | +| `/admin/developerkit/components/design/:id` | `ComponentCodeLayout` (liste tarafı ListForm ekranıdır) | `App.DeveloperKit.CustomComponents` | | `/admin/reports/:report/{view,design}/:id?/:listFormCode?` | `DevexpressReportViewer` · `DevexpressReportDesigner` | — | | `/admin/public/{home,about,services,contact}/designer` | ilgili public sayfa (tasarım modu) | `App.Home` · `App.About` · `App.Services` · `App.Contact` | | `/admin/settings` | `settings/Settings` | `App.Setting` | diff --git a/.github/instructions/lowcode.instructions.md b/.github/instructions/lowcode.instructions.md index f122cff1..50b2c81c 100644 --- a/.github/instructions/lowcode.instructions.md +++ b/.github/instructions/lowcode.instructions.md @@ -32,7 +32,7 @@ Bir istek geldiğinde çekirdek koda dokunmak değil, `configs/seeds/` altına u ## 0. Üretim protokolü (her prompt için zorunlu) Kullanıcı "şöyle bir ekran/form/süreç istiyorum" dediğinde **uygulamayı kullanıyormuş gibi** -davran: Wizard'ın, Component Manager'ın ve SQL Query Manager'ın diske yazacağı dosyaların +davran: Wizard'ın, Custom Components ekranının ve SQL Query Manager'ın diske yazacağı dosyaların aynılarını `configs/seeds/host/` altına sen yaz. Ekranlara tıklamanın yerine geçen şey budur. Talep iki biçimde gelir ve ikisi de aynı hatta girer: @@ -52,9 +52,10 @@ configs/seeds/host/ ├── sql/execute/{Ad}.sql ← bir kez çalışacak script (veri dolumu, migrasyon) ├── postgres/object|execute/… ← aynısının PostgreSQL diyalekti (hedef PG ise) ├── crud/{EntityName}.json ← tablonun REST uçları -├── custom/{ComponentName}.json ← Custom Component (Visual Designer dokümanı ya da kod) ├── wizard/{WizardName}.json ← ekran + menü + yetki + dil anahtarları └── data/{ListFormCode}.json ← ekrandan girilen liste verisinin aynası (opsiyonel) + Custom Component'ler de burada yaşar: + data/App.DeveloperKit.CustomComponents.json ``` Tenant kapsamı isteniyorsa `host/` yerine `tenants/{tenantId}/`; iç düzen aynıdır. @@ -65,7 +66,7 @@ Tenant kapsamı isteniyorsa `host/` yerine `tenants/{tenantId}/`; iç düzen ayn | --- | --- | --- | --- | | 1 | Tablo / view | `sql/object/…` | Ekranın bağlanacağı nesne önce var olmalı | | 2 | CRUD uçları (gerekiyorsa) | `crud/{Entity}.json` | Custom Component'in data source'ları buna bakar | -| 3 | Custom Component (gerekiyorsa) | `custom/{Name}.json` | Wizard `Custom` yolunda bileşen adını arar | +| 3 | Custom Component (gerekiyorsa) | `data/App.DeveloperKit.CustomComponents.json` | Wizard `Custom` yolunda bileşen adını arar | | 4 | Wizard | `wizard/{Ad}.json` | Menü, yetki, dil anahtarı ve ListForm'u üretir | | 5 | Alt ekranlar (varsa) | ek `wizard/…` dosyaları | Ana wizard'dan **önceki** `SeededAt` damgasını alır | | 6 | Liste verisi (isteniyorsa) | `data/{ListFormCode}.json` | Ekran ve tablo hazır olmalı; `ListForm.SeedFilePath` bu yolu gösterir | @@ -105,7 +106,7 @@ Uydurma. Şu üçünü mutlaka kontrol et: | JSON alan adları | **PascalCase** (`Wizard`, `ListFormCode`, `Groups`) — seeder `PropertyNameCaseInsensitive` okur ama dosyalar PascalCase üretilir | | Custom component dosyası | Kök alanlar PascalCase; `Props` **içindeki** designer dokümanı camelCase (`visualDesigner`, `nodes`, `sourceMode`) | | Enum'lar | Sayı olarak yazılır (§3) | -| `GeneratedAt` | ISO-8601 UTC (`2026-08-26T10:15:00Z`) — yalnızca bilgi amaçlı. **Yalnızca `custom/` ve `crud/` dosyalarında vardır**; wizard seed dosyasında karşılığı `Wizard.SeededAt` alanıdır | +| `GeneratedAt` | ISO-8601 UTC (`2026-08-26T10:15:00Z`) — yalnızca bilgi amaçlı. **Yalnızca `crud/` ve `data/` dosyalarında vardır**; wizard seed dosyasında karşılığı `Wizard.SeededAt` alanıdır | | İç içe JSON | `EditorOptions`, `EditorScript`, `LookupQuery` → JSON **string** olarak kaçışlanır | | Satır sonu | LF | @@ -1994,20 +1995,26 @@ ListForm ile ifade edilemeyen ekranın yolu **fiziksel React sayfası değil**, ### 7.1 Seed dosyası -`configs/seeds/{kapsam}/custom/{Name}.json` +`configs/seeds/{kapsam}/data/App.DeveloperKit.CustomComponents.json` — Custom Components +ekranının veri dosyası. Bileşenin **kendi dosyası yoktur**; her bileşen bu dosyada bir satırdır +ve dosya `data/{ListFormCode}.json` sözleşmesini (§0.1) izler. ```jsonc { + "ListFormCode": "App.DeveloperKit.CustomComponents", + "KeyFieldName": "Id", "GeneratedAt": "2026-08-20T11:23:45Z", - "CustomComponents": [ + "Order": 0, + "Rows": [ { + "Id": "8389ccbc-3e7b-738d-35aa-3a237eea9b9d", "Name": "OrderBoard", "RoutePath": "/admin/order-board", - "Code": "/*__SOZSOFT_VISUAL_DESIGNER____*/\nconst OrderBoard = () => { … }\n\nexport default OrderBoard", - "Props": "{\"visualDesigner\":{ … }}", "Description": "", "IsActive": true, "Dependencies": ["OrderCard"], + "Code": "/*__SOZSOFT_VISUAL_DESIGNER____*/\nconst OrderBoard = () => { … }\n\nexport default OrderBoard", + "Props": "{\"visualDesigner\":{ … }}", "DataSources": [ { "Name": "Mrp_T_Order — GetList", @@ -2024,6 +2031,9 @@ ListForm ile ifade edilemeyen ekranın yolu **fiziksel React sayfası değil**, } ``` +- `Id` satırın anahtarıdır (`KeyFieldName`). Yeni bir bileşen yazarken üret; import ve seed + eşleşmesi bu değer üzerinden yapılır. +- Yeni bir bileşen dosyaya **satır olarak eklenir**; var olan satırlar korunur. - `DataSources` elle yazılmaz: kaydetme sırasında `CustomComponentDataSourceResolver` tasarımcı dokümanındaki `dataSources` listesini `method + path` ile CRUD endpoint kataloğunda arayarak üretir. Eşleşenler `EntityName` + `SeedFile` taşır; elle yazılmış endpoint'ler listede @@ -2346,7 +2356,7 @@ Form düğümünün `id`'si) ve Save/Delete üzerinden geri yazar. Olaylar: `onL - `Özel` mod: yetki adı elle yazılır. - Boş bırakılan komut serbesttir. -Manager ekranı yetkileri: `App.DeveloperKit.Components{,.Create,.Update,.Delete}`. +Liste ekranı yetkileri: `App.DeveloperKit.CustomComponents{,.Create,.Update,.Delete}`. ### 7.6 Bileşeni menüye bağlama @@ -2397,7 +2407,7 @@ Custom yolunda `ListForm` **üretilmez**; `Title`/`Desc` dil anahtarları da ür Bu çift, custom component'in menüye nasıl bağlandığının çalışan örneğidir. İncelenecek dosyalar: ``` -configs/seeds/host/custom/RoleComponent.json +configs/seeds/host/data/App.DeveloperKit.CustomComponents.json ← RoleComponent satırı configs/seeds/host/crud/AbpRoles.json · AbpUsers.json · AbpUserRoles.json configs/seeds/host/wizard/RoleList.json ``` @@ -2438,14 +2448,22 @@ butonu zip üretir: ``` wizard/{dosya}.json -custom/{component}.json (bağımlılıklarıyla) +data/App.DeveloperKit.CustomComponents.json (component + bağımlılıklarına süzülmüş kopya) crud/{entity}.json (component'lerin kullandığı endpoint'ler) {sql|postgres}/{object|execute}/{nesne}.sql (List yolunda SelectCommand'ın nesnesi) ``` -Import iki adımlıdır: analiz (`New`/`Identical`/`Conflict`, çakışmalar diff editörde çözülür) → -uygulama (dosya dosya yazım, önceki hâl yedeklenir, `RollbackImport` ile toplu geri alma). -Sınırlar: yalnızca `wizard`, `crud`, `custom`, `sql`, `postgres` kök klasörleri (SQL sağlayıcı +Import üç adımlıdır: analiz (`New`/`Identical`/`Conflict`, çakışmalar diff editörde çözülür; +`data/*.json` satır bazlı karşılaştırılır — yalnızca zip'in taşıdığı anahtarlar, dolayısıyla +ilgisiz component'ler fark olarak görünmez) → +uygulama (dosya dosya yazım, önceki hâl yedeklenir, `RollbackImport` ile toplu geri alma) → +kapanış (`CompleteImport`: yazılan `data/*.json` dosyaları `ListFormSeedDataApplier` ile +veritabanına uygulanır — tabloda olmayan satır eklenir, var olan satır dosyadaki değerlerle +güncellenir, soft delete edilmiş satırın `IsDeleted`/`DeletionTime`/`DeleterId` izleri temizlenir. Bu adım rollback kapsamında değildir; `wizard`/`crud`/`sql` dosyaları seeder ya da +deploy bekler). +`data/*.json` dosyaları kapsamdaki tüm satırları taşıdığı için üzerine yazılmaz: satırlar anahtar +alan üzerinden birleştirilir (gelen satır varsa güncellenir, yoksa eklenir, hedefteki diğer satırlar +korunur). Sınırlar: yalnızca `wizard`, `crud`, `data`, `sql`, `postgres` kök klasörleri (SQL sağlayıcı klasörlerinin altında yalnızca `object`/`execute`); 5 MB/dosya, 50 MB/arşiv, 500 girdi. Yetkiler: `App.Listforms.Wizard.Export` / `.Import`. @@ -2460,7 +2478,7 @@ ref'iyle üretildiyse geri okuma da onunla yapılır. Script'ler `Props.visualDesigner` içinde saklanır ve kod üreticisi tarafından tek argümanlı (`event`) handler gövdesine gömülür. Metin gömüldüğü için **her artefakt gibi seed dosyasına da -yazılır**; `custom/{Name}.json` güncellenmeden bırakılan bir script veritabanı sıfırlandığında +yazılır**; Custom Components ekranının seed verisi güncellenmeden bırakılan bir script veritabanı sıfırlandığında geri gelmez. #### Olaylar @@ -3029,7 +3047,7 @@ bağlıdır; hiçbiri "kod yaz" adımının yerine geçmez, hepsi ondan öncedir | **SQL Query Manager** | `/admin/sqlQueryManager` | `App.SqlQueryManager` | SQL nesneleri, CRUD endpoint'ler | 9.5 | | **Custom Endpoints** | `/admin/list/App.DeveloperKit.CustomEndpoints` | `App.DeveloperKit.CustomEndpoints` | Elle yazılmış SQL'den REST endpoint | 9.2 | | **Dynamic Services** | `/admin/list/App.DeveloperKit.DynamicServices` | `App.DeveloperKit.DynamicServices` | Runtime derlenen C# AppService | 9.3 | -| **Components** | `/admin/developerkit/components` | `App.DeveloperKit.Components` | Custom Component + Visual Designer | 7 | +| **Components** | `/admin/list/App.DeveloperKit.CustomComponents` | `App.DeveloperKit.CustomComponents` | Custom Component + Visual Designer | 7 | | **ListForm** | `/admin/list/App.Listforms.Listform` | `App.Listforms.Listform` | Var olan ekranların ham tanımı | 9.6 | | **Wizard Manager** | `/admin/listform/wizardManager` | `App.Listforms.Wizard` | Wizard seed dosyalarının yönetimi, export/import | 4, 7.7 | @@ -3502,13 +3520,13 @@ Kurallar: ### 10.3 "Serbest yerleşimli sayfa / dashboard" 1. Gerekli endpoint'ler yoksa CRUD Endpoint üret. -2. Component Manager → yeni bileşen → Visual Designer: +2. Custom Components ekranı → yeni satır → **Design** → Visual Designer: `PageContainer` → `FlexRow` → içine `platform` düğümleri (`listFormCode` ile) ve/veya `Form`. 3. Veri kaynaklarını Data sekmesinden bağla; filtreleri `route`/`query`/`record` kaynaklarıyla kur, `required` bayrağını bilinçli seç. 4. Görünürlük ve komut yetkilerini ver (`designerPermission`, `*Permission`). 5. Wizard `ComponentKind: 1` ile menüye bağla. -6. Doğrula: `custom/{Name}.json` ve ilgili `crud/*.json` dosyaları oluştu mu. +6. Doğrula: `data/App.DeveloperKit.CustomComponents.json` içindeki satır ve ilgili `crud/*.json` dosyaları oluştu mu. ### 10.4 "Kanban panosu" @@ -3554,13 +3572,13 @@ Bu tür isteklerin tamamı aynı iskeleti kullanır. Kullanıcı "avans talep fo configs/seeds/host/ ├── sql/object/Hr_T_AdvanceRequest.sql ← tablo (+ PostgreSQL hedefse postgres/object/…) ├── crud/Hr_T_AdvanceRequest.json ← yalnızca Custom Component yolunda gerekir -├── custom/AdvanceRequestForm.json ← yalnızca serbest yerleşim isteniyorsa +├── data/App.DeveloperKit.CustomComponents.json ← yalnızca serbest yerleşim isteniyorsa ├── wizard/AdvanceRequestApprovals.json ← (varsa) menüsüz alt ekran, küçük SeededAt └── wizard/AdvanceRequests.json ← ana ekran + menü + yetki + dil + onay akışı ``` **Karar:** Talep formu **standart bir kayıt listesi + düzenleme formu + onay akışı** ise -`ComponentKind: 0` (List) yeter ve `custom/` + `crud/` dosyalarına hiç gerek yoktur. Custom +`ComponentKind: 0` (List) yeter ve component seed verisi + `crud/` dosyalarına hiç gerek yoktur. Custom Component yoluna yalnızca serbest yerleşim (özet kartlar, çok adımlı sihirbaz, gömülü panolar) isteniyorsa geçilir. diff --git a/README.md b/README.md index 7849f8c2..6e077e18 100644 --- a/README.md +++ b/README.md @@ -245,17 +245,17 @@ runtime'da düzenlenebilen seed'ler `configs/seeds/` altındadır — bkz. `App: `configs/seeds` altında en üst seviye kapsamdır (CDN klasörleriyle aynı mantık): host veritabanı için `host/`, her tenant için `tenants/{tenantId}/`. Kapsamın altında içerik -türüne göre `crud/`, `custom/`, `wizard/`, `data/`, `sql/`, `postgres/` klasörleri bulunur: +türüne göre `crud/`, `wizard/`, `data/`, `sql/`, `postgres/` klasörleri bulunur: ``` configs/seeds/ host/ - crud/ custom/ wizard/ data/ + crud/ wizard/ data/ sql/ { object/ execute/ } postgres/ { object/ execute/ } tenants/ {tenantId}/ - crud/ custom/ wizard/ data/ + crud/ wizard/ data/ sql/ { object/ execute/ } postgres/ { object/ execute/ } ``` @@ -269,8 +269,7 @@ Aktif veritabanı sağlayıcısına göre `sql/` (SQL Server) ya da `postgres/` | `{sql\|postgres}/execute` | Nesneyi oluşturur; ayrıca tüm migration ve seed'ler bittikten sonra `AfterAllMigrationsSqlExecutor` dosya adından türettiği procedure'ü çalıştırır (dosya adı = procedure adı, `schema.Procedure.sql` de olur). | | `wizard` | ListForm Wizard'ın ürettiği `.json` konfigürasyonları; `WizardDataSeeder` kendi kapsamındaki dosyaları okur. | | `crud` | CrudEndpoint konfigürasyonları; `CrudDataSeeder` kendi kapsamındaki dosyaları okur. | -| `custom` | Custom Component konfigürasyonları (`{Name}.json`); Component Manager kaydetme/silme sırasında dosyayı senkronlar, `CustomComponentDataSeeder` kendi kapsamındaki dosyaları okuyup veritabanına uygular. | -| `data` | Ekranlardan girilen listelerin veri aynası (`{ListFormCode}.json`). `ListForm.SeedFilePath` dolu olan ekranda `SeedSyncInsert/Update/Delete` bayraklarından işaretli olan işlemler dosyaya yansır (`ListFormSeedDataSynchronizer`); migrate/seed sırasında bütün contributor'lar bittikten sonra `ListFormSeedDataApplier` dosyayı geri uygular: anahtarı veritabanında **olmayan** satırları ekler, var olanlara dokunmaz. | +| `data` | Ekranlardan girilen listelerin veri aynası (`{ListFormCode}.json`). `ListForm.SeedFilePath` dolu olan ekranda `SeedSyncInsert/Update/Delete` bayraklarından işaretli olan işlemler dosyaya yansır (`ListFormSeedDataSynchronizer`); migrate/seed sırasında bütün contributor'lar bittikten sonra `ListFormSeedDataApplier` dosyayı geri uygular: anahtarı veritabanında **olmayan** satırları ekler, var olanlara dokunmaz. Aynı uygulayıcı wizard import'unun kapanışında da (`CompleteImport`) yalnızca o import'ta yazılan dosyalar için, `overwriteExisting` açık olarak çalışır — orada var olan satır dosyadaki değerlerle güncellenir. | Klasör kökünde kalmış eski `.sql` dosyaları geriye dönük uyumluluk için hâlâ işlenir, fakat `SqlDataSeeder` bunları uyarı ile loglar — ilgili klasöre taşınmaları beklenir. @@ -554,8 +553,10 @@ Yetkiler, Alt Form, Pivot, Tree, Gantt, Scheduler, Todo, Widget, Workflow, Durum **Wizard File Manager** (`/admin/listform/wizard` liste ekranı) üretilmiş seed dosyalarını yönetir: liste/kart görünümü arasında geçiş (tercih diğer listelerle aynı yerde, `admin.lists.states` altında saklanır), arama, düzenleme (dosyanın yerine geçen yeni bir çalıştırma), silme, veritabanı -migrate tetikleme, dışa aktarma ve içe aktarma. Butonlar `App.Listforms.Wizard.*` yetkileriyle -gizlenir; asıl kontrol `ListFormWizardAppService` üzerindedir. +migrate tetikleme, dışa aktarma ve içe aktarma. Liste, wizard'ın ilk deploy damgasına +(`Wizard.SeededAt`) göre **tersine** sıralıdır — en son üretilen ekran en üstte durur; damgası +okunamayan dosyalar sona düşer. Butonlar `App.Listforms.Wizard.*` yetkileriyle gizlenir; asıl +kontrol `ListFormWizardAppService` üzerindedir. ### 7.7. İçe/dışa aktarma @@ -575,21 +576,46 @@ aynı klasör düzenine doğrudan açılabilir. | Yol | İçerik | | --- | --- | | `wizard/{dosya}.json` | Wizard tanımının kendisi | -| `custom/{component}.json` | Custom yolunda bağlı component ve bağımlılıkları | +| `data/App.DeveloperKit.CustomComponents.json` | Custom yolunda bağlı component ve bağımlılıkları — dosyanın yalnızca bu satırlara süzülmüş kopyası | | `crud/{entity}.json` | Component'lerin/nesnenin kullandığı CRUD endpoint tanımları | | `{sql\|postgres}/{object\|execute}/{nesne}.sql` | List yolunda `SelectCommand`'a karşılık gelen SQL nesnesi | -**İçe aktarma iki adımlıdır** (`WizardImportDialog`): +**İçe aktarma üç adımlıdır** (`WizardImportDialog`): 1. **Analiz** (`AnalyzeImport`) — zip, seed kökü altındaki `.imports/{importId}/staged` klasörüne açılır (nokta ile başladığı için seeder taramalarına girmez) ve her dosya hedefteki karşılığıyla karşılaştırılır: `New` (hedefte yok), `Identical` (aynı, sorulmaz), `Conflict` (farklı). Çakışan dosyalar Monaco `DiffEditor` ile karşılaştırılıp birleştirilir. + `data/*.json` girdilerinde karşılaştırma **satır bazlıdır**: hedef dosyadan yalnızca gelen + satırların anahtarları süzülür, dosya düzeyindeki `GeneratedAt`/`Order` karşılaştırmaya girmez. + Böylece aktarılan kaydın dışındaki satırlar fark olarak görünmez; gelen satırların hiçbiri + hedefte yoksa dosya var olsa bile durum `New` (ekleme), varsa ve içerik aynıysa `Identical` + (hiç aktarılmaz), farklıysa `Conflict` (güncelleme) olur. 2. **Uygulama** (`ApplyImport`) — dosyalar tek tek yazılır, ilerleme satır satır gösterilir. Yazılan her dosyanın önceki hâli `.imports/{importId}/backup` altına alındığı için hata durumunda - `RollbackImport` ile toplu işlem geri alınabilir; `CompleteImport` oturumu kapatır. + `RollbackImport` ile toplu işlem geri alınabilir. +3. **Kapanış** (`CompleteImport`) — oturum kapatılmadan önce zip'in getirdiği `data/*.json` dosyaları + `IListFormSeedDataApplier.ApplyFilesAsync(..., overwriteExisting: true)` ile veritabanına + uygulanır: tabloda olmayan satır eklenir, var olan satır dosyadaki değerlerle **güncellenir** + (migrate/seed akışından farkı budur — orada dosya bayat olabileceği için canlı kayda + dokunulmaz). Kayıt daha önce **soft delete** edilmişse silinme izleri de temizlenir + (`IsDeleted`, `DeletionTime`, `DeleterId`); aksi halde satır tabloda durur ama ekranın + varsayılan süzgeci (`"IsDeleted" = 'false'`) onu gizlemeye devam ederdi. Hangi kolonların + sıfırlanacağı ekranın `DeleteCommand`'ından okunur, hard delete yapan ekranda hiçbir şey + eklenmez. Hedefle **zaten aynı** olan (`Identical`) veri dosyaları da uygulanır: yazacak bir şey + yoktur ama satırlar veritabanında eksik ya da silinmiş olabilir. Kullanıcının "mevcudu koru" + dediği (farklı olup yazılmayan) dosyaya dokunulmaz. Anahtarı olmayan satır güncellenemez, + atlanır; eklenen/güncellenen/atlanan satır + sayısı dialogda raporlanır. Bu adım `RollbackImport` + kapsamında **değildir** — geri alma bu adımdan önce yapılır. Diğer klasörler (`wizard`, `crud`, + `sql`) dosya olarak kalır; veritabanına seeder ya da wizard deploy ile geçer. -Güvenlik sınırları: yalnızca `wizard`, `crud`, `custom`, `sql`, `postgres` kök klasörleri +`data/*.json` dosyaları kapsamdaki bütün satırları taşıdığı için üzerine yazılmaz: satırlar anahtar +alan üzerinden birleştirilir (gelen satır varsa günceller, yoksa eklenir, hedefteki diğer satırlar +korunur). Diff ekranında düzenlenen içerik de aynı sözleşmeyi taşıdığı için elle birleştirme bu +davranışı bozmaz. + +Güvenlik sınırları: yalnızca `wizard`, `crud`, `data`, `sql`, `postgres` kök klasörleri (SQL sağlayıcı klasörlerinin altında yalnızca `object` ve `execute`) kabul edilir; dosya başına 5 MB, arşiv başına 50 MB, en çok 500 girdi. Kabul edilmeyen girdiler analiz sonucunda uyarı olarak listelenir. Yetkiler: `App.Listforms.Wizard.Export` / `App.Listforms.Wizard.Import`. @@ -606,7 +632,7 @@ Teknik kullanıcılar için `/admin/developerkit` altında toplanan araçlar: | **Custom Endpoint** | `/admin/list/App.DeveloperKit.CustomEndpoints` | SQL veya servis tanımından REST endpoint üretimi; GET/POST/PUT/DELETE için ayrı yetki kapıları (`App.DeveloperKit.CustomEndpoints.*`) ve kullanıcı/rol/global erişim kuralları. | | **CRUD Endpoint** | SQL Query Manager içinde | Tablo tanımından tam CRUD endpoint kümesi üretimi. Ayrı ekranı yoktur; bkz. aşağıdaki not. | | **Dynamic Service** | `/admin/list/App.DeveloperKit.DynamicServices` | Liste bir ListForm ekranıdır (KPI şeridi, Swagger butonu, kart/ızgara görünümü); kod yazma listeden açılan `DynamicServiceEditor` diyalogundadır: Monaco editörü, Roslyn ile derleme (`TestCompile`), yayınlama (`Publish`) ve çalışan uygulamaya controller olarak kaydetme. Yetkiler: Create/Edit/Delete/Manage/TestCompile/Publish/ViewCode. | -| **Custom Component** | `/admin/developerkit/components` | React bileşenini veritabanında saklama; `@babel/standalone` ile tarayıcıda derleyip route'a bağlama. Liste/kart görünümü, arama; yetkiler `App.DeveloperKit.Components{,.Create,.Update,.Delete}`. | +| **Custom Component** | `/admin/list/App.DeveloperKit.CustomComponents` | React bileşenini veritabanında saklama; `@babel/standalone` ile tarayıcıda derleyip route'a bağlama. Liste bir ListForm ekranıdır (KPI şeridi, ızgara/kart görünümü, grid'in kendi ekleme/düzenleme formu); satırdaki **Design** butonu `/admin/developerkit/components/design/{id}` tasarımcısını, **Open URL** butonu bileşenin `RoutePath`'ini açar. Kayıtlar `configs/seeds/host/data/App.DeveloperKit.CustomComponents.json` dosyasına senkronlanır. Yetkiler `App.DeveloperKit.CustomComponents{,.Create,.Update,.Delete}`. | | **Visual Designer** | Component editörü içinde | Sürükle-bırak kanvas ile bileşen üretimi ve kod üretimi (`visualDesigner/codeGenerator.ts`). | | **ListForm** | `/admin/list/App.Listforms.Listform` | Var olan ekranların ham tanım listesi; buradan `/admin/listform/edit/{kod}` sekmeli editörüne geçilir. | | **Wizard Manager** | `/admin/listform/wizardManager` | Üretilmiş wizard seed dosyalarının listesi/kartı, düzenleme, silme, DB migrate, export/import. Yetki `App.Listforms.Wizard{,.Create,.Update,.Delete,.Export,.Import}`. | @@ -665,16 +691,16 @@ satırındaki `/*__SOZSOFT_VISUAL_DESIGNER__…__*/` yorumunda. Böylece yalnız tasarımcı dokümanı geri okunabilir. `sourceMode` alanı `visual` ya da `code` olur; kod moduna geçilen bir bileşen kanvasa geri dönmez. -**Seed senkronizasyonu.** Component Manager üzerinden kaydedilen/silinen her bileşen -`configs/seeds/{host|tenants/{tenantId}}/custom/{Name}.json` olarak yazılır; -`CustomComponentDataSeeder` veritabanı silinip yeniden oluşturulduğunda aynı dosyaları okuyup geri -yükler. Dosya düzeni bilinçli olarak `TenantData.json` içindeki `CustomComponents` bloğuyla -aynıdır. `DataSources` sütunu tasarımcı dokümanından türetilir -(`CustomComponentDataSourceResolver`): Data sekmesindeki her endpoint `method + path` ile CRUD -endpoint kataloğunda aranır, eşleşenler `EntityName` + `crud/{EntityName}.json` referansını taşır, -elle yazılmış olanlar listede kalır ama bu alanları boş gelir. Aynı çözüm hem kaydetmede (katalog -veritabanından) hem seed'de (katalog `crud/*.json` dosyalarından) kullanıldığı için sütun iki -yolda da aynı üretilir. +**Seed senkronizasyonu.** Bileşenlerin seed kaynağı Custom Components ekranının veri dosyasıdır: +`configs/seeds/{host|tenants/{tenantId}}/data/App.DeveloperKit.CustomComponents.json`. Bileşene +yazan **iki yol** da bu dosyaya işlenir (`ListFormSeedDataSynchronizer`): listeden yapılan +ekleme/güncelleme/silme ve Visual Designer'ın kaydı (`CustomComponentAppService`). Veritabanı +sıfırlandığında `ListFormSeedDataApplier` satırları geri yazar; `Code`, `Props` ve `DataSources` +kolonları bu yüzden ekranda gizli alan olarak tanımlıdır. `DataSources` sütunu tasarımcı +dokümanından türetilir (`CustomComponentDataSourceResolver`): Data sekmesindeki her endpoint +`method + path` ile CRUD endpoint kataloğunda aranır, eşleşenler `EntityName` + +`crud/{EntityName}.json` referansını taşır, elle yazılmış olanlar listede kalır ama bu alanları +boş gelir. Wizard export'u bu referansları izleyerek bağlı `crud/*.json` dosyalarını zip'e ekler. **Toolbox.** Kanvasa bırakılabilecekler beş aileye ayrılır: @@ -715,7 +741,8 @@ parametresi (`?RoleId=…`), diğerleri son ek taşır (`?Name.contains=…`). O **Diğer notlar.** `PlatformIcon` tasarımcı dokümanındaki ikon adını çözer; `selectComponents.ts` `Select.componentAs` için saklanan adı (`ReactSelect`, `CreatableSelect`, `AsyncSelect`) gerçek bileşene çevirir — kanvas ve üretilen bileşenin aynı adı aynı şekilde yorumlamasını sağlayan tek -nokta budur. Yeni bileşen `configs/seeds/host/custom/NewComponent.json` şablonundan başlar. +nokta budur. Yeni bileşen `configs/seeds/host/data/App.DeveloperKit.CustomComponents.json` +içindeki bir satır olarak başlar. **Dinamik servis yaşam döngüsü:** `DynamicServiceCompiler` kodu derler → `DynamicAssemblyRegistrationService` assembly'yi tenant bağlamıyla kaydeder → @@ -756,7 +783,7 @@ içindedir; oradaki kontroller yalnızca butonları gizler, asıl kontrol AppSer | Grup | Alt yetkiler | | --- | --- | | `App.Listforms.Wizard` | `.Create`, `.Update`, `.Delete`, `.Export`, `.Import` | -| `App.DeveloperKit.Components` | `.Create`, `.Update`, `.Delete` | +| `App.DeveloperKit.CustomComponents` | `.Create`, `.Update`, `.Delete` | | `App.DeveloperKit.CustomEndpoints` | `.Get`, `.Post`, `.Put`, `.Remove` (dispatcher üzerinden çağrı kapısı; endpoint bazlı User/Role/Global kuralları bunun üstünde denetlenir) | | `App.DeveloperKit.DynamicServices` | `.Create`, `.Edit`, `.Delete`, `.Manage`, `.TestCompile`, `.Publish`, `.ViewCode` | | `App.SqlQueryManager` | `.CrudEndpoints` | @@ -1168,18 +1195,17 @@ derlenip yayınlanır. Yeni bir modülün nasıl kurulacağını öğrenmek içi - `PermissionsData.json` — yetki grupları ve tanımları - `HostData.json` — ayar/entegrasyon değerleri - `LanguagesData.json` — dil anahtarları (EN/TR) -- `WizardDataSeeder.cs`, `CustomComponentDataSeeder.cs`, `CrudDataSeeder.cs` — aşağıdaki +- `WizardDataSeeder.cs`, `CrudDataSeeder.cs` — aşağıdaki runtime seed dosyalarını okuyup uygulayan seeder'lar -**2. Runtime'da üretilen seed'ler** — `configs/seeds/`. Wizard, Component Manager ve CRUD Endpoint -Manager çalışma zamanında burayı yazar; veritabanı silinip yeniden oluşturulduğunda aynı dosyalar +**2. Runtime'da üretilen seed'ler** — `configs/seeds/`. Wizard, Custom Components ekranı/Visual +Designer ve CRUD Endpoint Manager çalışma zamanında burayı yazar; veritabanı silinip yeniden oluşturulduğunda aynı dosyalar geri yüklenir. Kapsam klasörü CDN düzeniyle aynıdır ve `SeedPathResolver` üzerinden çözülür: ``` configs/seeds/ ├── host/ # host kapsamı │ ├── wizard/ # {Ad}.json → WizardDataSeeder -│ ├── custom/ # {ComponentName}.json → CustomComponentDataSeeder │ ├── crud/ # {EntityName}.json → CrudDataSeeder │ ├── sql/{object,execute}/ # .sql (SQL Server) │ └── postgres/{object,execute}/ diff --git a/api/src/Sozsoft.Platform.Application.Contracts/DeveloperKit/ICustomComponentAppService.cs b/api/src/Sozsoft.Platform.Application.Contracts/DeveloperKit/ICustomComponentAppService.cs index 384b9dd1..4d9229ac 100644 --- a/api/src/Sozsoft.Platform.Application.Contracts/DeveloperKit/ICustomComponentAppService.cs +++ b/api/src/Sozsoft.Platform.Application.Contracts/DeveloperKit/ICustomComponentAppService.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Threading.Tasks; using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Services; @@ -12,7 +10,4 @@ public interface ICustomComponentAppService : ICrudAppService< PagedAndSortedResultRequestDto, CreateUpdateCustomComponentDto> { - Task> GetActiveComponentsAsync(); } - - diff --git a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/IListFormWizardAppService.cs b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/IListFormWizardAppService.cs index df26f12a..9ab8c91f 100644 --- a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/IListFormWizardAppService.cs +++ b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/IListFormWizardAppService.cs @@ -27,7 +27,7 @@ public interface IListFormWizardAppService Task ApplyImport(WizardImportApplyInput input); /// Import oturumunu basariyla kapatir ve staging/yedek klasorunu siler. - Task CompleteImport(string importId); + Task CompleteImport(string importId); /// O ana kadar yazilan dosyalari geri alir ve staging klasorunu siler. Task RollbackImport(string importId); diff --git a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardImportDtos.cs b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardImportDtos.cs index 8411459c..f82c2143 100644 --- a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardImportDtos.cs +++ b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardImportDtos.cs @@ -9,10 +9,10 @@ namespace Sozsoft.Platform.ListForms; /// public class WizardImportEntryDto { - /// Kapsam klasorune goreli yol, ornegin custom/Orders.json. + /// Kapsam klasorune goreli yol, ornegin crud/Orders.json. public string Path { get; set; } - /// Yolun ilk segmenti; listede gruplama icin kullanilir (wizard, crud, custom, sql, postgres). + /// Yolun ilk segmenti; listede gruplama icin kullanilir (wizard, crud, data, sql, postgres). public string Folder { get; set; } public string FileName { get; set; } @@ -70,3 +70,31 @@ public class WizardImportApplyResultDto public long Size { get; set; } } + +/// +/// Import kapanisinin sonucu. Yazilan data/*.json dosyalari ayni islemde veritabanina +/// da uygulanir; her dosyanin sonucu burada raporlanir. +/// +public class WizardImportCompleteResultDto +{ + public List AppliedData { get; set; } = []; +} + +/// Tek bir veri seed dosyasinin veritabanina uygulanma sonucu. +public class WizardImportSeedApplyResultDto +{ + /// Kapsam klasorune goreli yol. + public string Path { get; set; } + + /// Veritabanina eklenen satir sayisi. + public int InsertedRowCount { get; set; } + + /// Zaten var oldugu icin dosyadaki degerlerle guncellenen satir sayisi. + public int UpdatedRowCount { get; set; } + + /// Anahtari olmadigi icin guncellenemeyen, dokunulmayan satir sayisi. + public int SkippedRowCount { get; set; } + + /// Dosya uygulanamadiysa nedeni; basarili durumda bostur. + public string Error { get; set; } +} diff --git a/api/src/Sozsoft.Platform.Application/DeveloperKit/CustomComponentAppService.cs b/api/src/Sozsoft.Platform.Application/DeveloperKit/CustomComponentAppService.cs index d8d28949..8e6d1541 100644 --- a/api/src/Sozsoft.Platform.Application/DeveloperKit/CustomComponentAppService.cs +++ b/api/src/Sozsoft.Platform.Application/DeveloperKit/CustomComponentAppService.cs @@ -1,16 +1,15 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Text.Encodings.Web; using System.Text.Json; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Sozsoft.Platform.Data.Seeds; using Sozsoft.Platform.DeveloperKit; using Sozsoft.Platform.Entities; +using Sozsoft.Platform.ListForms; using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Services; using Volo.Abp.Domain.Repositories; @@ -21,9 +20,13 @@ namespace Platform.Api.Application; /// /// Custom component yonetimi. Sinif seviyesindeki yalnizca /// kimlik dogrulamasi ister; ekrani gorme ve CRUD haklari asagida ABP policy adlarina baglanir. -/// App.DeveloperKit.Components okuma, .Create/.Update/.Delete aksiyon -/// yetkileridir. calisma zamani okuma yoludur ve yalnizca -/// oturum acmis olmayi ister. +/// App.DeveloperKit.CustomComponents okuma, .Create/.Update/.Delete aksiyon +/// yetkileridir. +/// +/// Component'ler App.DeveloperKit.CustomComponents ekraninin verisidir; ekran uzerinden +/// yapilan kayitlar seed dosyasina ListForm altyapisi tarafindan yansitilir. Designer bu servisi +/// kullandigi icin ayni yansitma burada da tetiklenir, aksi halde dosya bayat kalir. +/// /// [Authorize] public class CustomComponentAppService : CrudAppService< @@ -40,21 +43,19 @@ public class CustomComponentAppService : CrudAppService< Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }; - private static readonly JsonSerializerOptions JsonReadOptions = new() - { - PropertyNameCaseInsensitive = true - }; + /// Component'lerin verisini tasiyan ekran; seed dosyasi bu kod uzerinden bulunur. + private const string ListFormCode = SeedConsts.AppCodes.DeveloperKits.CustomComponents; - private readonly IConfiguration _configuration; private readonly IRepository _crudEndpointRepository; + private readonly IListFormSeedDataSynchronizer _seedDataSynchronizer; public CustomComponentAppService( IRepository repository, IRepository crudEndpointRepository, - IConfiguration configuration) : base(repository) + IListFormSeedDataSynchronizer seedDataSynchronizer) : base(repository) { _crudEndpointRepository = crudEndpointRepository; - _configuration = configuration; + _seedDataSynchronizer = seedDataSynchronizer; GetPolicyName = AppCodes.DeveloperKits.Components.Component; GetListPolicyName = AppCodes.DeveloperKits.Components.Component; @@ -67,7 +68,7 @@ public class CustomComponentAppService : CrudAppService< { var result = await base.CreateAsync(input); await SyncDataSourcesAsync(result.Id); - await SyncSeedFileAsync(result.Id); + await _seedDataSynchronizer.SyncRowAsync(ListFormCode, result.Id, SeedSyncOperation.Insert); return result; } @@ -76,7 +77,6 @@ public class CustomComponentAppService : CrudAppService< CreateUpdateCustomComponentDto input) { var existingComponent = await Repository.GetAsync(id); - var previousName = existingComponent.Name; if (input.Props is null) { @@ -85,109 +85,15 @@ public class CustomComponentAppService : CrudAppService< var result = await base.UpdateAsync(id, input); - // Ad degistiyse eski dosya artik hicbir component'i temsil etmez. - if (!string.Equals(previousName, result.Name, StringComparison.Ordinal)) - { - DeleteSeedFile(previousName); - } - await SyncDataSourcesAsync(id); - await SyncSeedFileAsync(id); + await _seedDataSynchronizer.SyncRowAsync(ListFormCode, id, SeedSyncOperation.Update); return result; } public override async Task DeleteAsync(Guid id) { - var component = await Repository.FindAsync(id); await base.DeleteAsync(id); - - if (component != null) - { - DeleteSeedFile(component.Name); - } - } - - public async Task> GetActiveComponentsAsync() - { - var components = await Repository.GetListAsync(x => x.IsActive); - return await MapToGetListOutputDtosAsync(components); - } - - /// - /// Seed dosyasini yazar; dosya islemi hatasi CRUD islemini engellemez, yalnizca loglanir. - /// - private async Task SyncSeedFileAsync(Guid id) - { - try - { - var component = await Repository.FindAsync(id); - if (component != null) - { - await WriteSeedFileAsync(component); - } - } - catch (Exception ex) - { - Logger.LogError(ex, "Custom component seed file could not be synchronized for {Id}: {Message}", id, ex.Message); - } - } - - private async Task WriteSeedFileAsync(CustomComponent component) - { - var safeFileName = GetSafeFileName(component.Name) - ?? throw new InvalidOperationException($"Invalid component name for a seed file: '{component.Name}'."); - - var outputPath = ResolveCustomSeedOutputPath(); - Directory.CreateDirectory(outputPath); - - var filePath = Path.Combine(outputPath, $"{safeFileName}.json"); - - var seedFile = new CustomComponentSeedFileDto - { - GeneratedAt = Clock.Now.ToUniversalTime(), - CustomComponents = - [ - new CustomComponentSeedItemDto - { - Name = component.Name, - RoutePath = component.RoutePath, - Code = component.Code, - Props = component.Props, - Description = component.Description, - IsActive = component.IsActive, - Dependencies = ParseDependencies(component.Dependencies), - DataSources = ParseDataSources(component.DataSources) - } - ] - }; - - await File.WriteAllTextAsync(filePath, JsonSerializer.Serialize(seedFile, SeedJsonWriteOptions)); - Logger.LogInformation("Custom component seed file saved: {FilePath}", filePath); - - return filePath; - } - - private void DeleteSeedFile(string componentName) - { - try - { - var safeFileName = GetSafeFileName(componentName); - if (safeFileName == null) - { - return; - } - - var filePath = Path.Combine(ResolveCustomSeedOutputPath(), $"{safeFileName}.json"); - if (File.Exists(filePath)) - { - File.Delete(filePath); - Logger.LogInformation("Custom component seed file deleted: {FilePath}", filePath); - } - } - catch (Exception ex) - { - Logger.LogError(ex, "Custom component seed file could not be deleted for {Name}: {Message}", componentName, ex.Message); - } + await _seedDataSynchronizer.RemoveRowsAsync(ListFormCode, [id]); } /// @@ -241,74 +147,4 @@ public class CustomComponentAppService : CrudAppService< return CustomComponentDataSourceResolver.Resolve(props, references); } - - /// Entity'de JSON string olarak tutulan endpoint listesini cozer. - private static List ParseDataSources(string? dataSources) - { - if (string.IsNullOrWhiteSpace(dataSources)) - { - return []; - } - - try - { - return JsonSerializer.Deserialize>(dataSources, JsonReadOptions) ?? []; - } - catch (JsonException) - { - return []; - } - } - - /// Designer dokumanindaki tek bir data source kaydi. - private sealed class DesignerDataSourceDto - { - public string? Name { get; set; } - public string? Method { get; set; } - public string? Url { get; set; } - public string? ResponsePath { get; set; } - } - - /// Entity'de JSON string olarak tutulan bagimlilik listesini cozer. - private static List ParseDependencies(string? dependencies) - { - if (string.IsNullOrWhiteSpace(dependencies)) - { - return []; - } - - try - { - return JsonSerializer.Deserialize>(dependencies, JsonReadOptions) ?? []; - } - catch (JsonException) - { - return []; - } - } - - /// - /// Custom component seed dosyalarinin yazilacagi klasor; kapsam aktif tenant'i izler - /// (host baglaminda host/custom, tenant baglaminda tenants/{tenantId}/custom). - /// - private string ResolveCustomSeedOutputPath() - { - return SeedPathResolver.GetScopePath(_configuration, CurrentTenant.Id, SeedPathResolver.CustomFolder); - } - - /// Component adindan guvenli bir dosya adi uretir; path traversal denemelerini reddeder. - private static string? GetSafeFileName(string componentName) - { - var trimmed = componentName?.Trim(); - if (string.IsNullOrWhiteSpace(trimmed) || - trimmed.Contains('/') || - trimmed.Contains('\\') || - trimmed.Contains("..")) - { - return null; - } - - var safeFileName = string.Concat(trimmed.Split(Path.GetInvalidFileNameChars())); - return string.IsNullOrWhiteSpace(safeFileName) ? null : safeFileName; - } } diff --git a/api/src/Sozsoft.Platform.Application/ListForms/ListFormWizardAppService.cs b/api/src/Sozsoft.Platform.Application/ListForms/ListFormWizardAppService.cs index 2dc83755..b6a5ee65 100644 --- a/api/src/Sozsoft.Platform.Application/ListForms/ListFormWizardAppService.cs +++ b/api/src/Sozsoft.Platform.Application/ListForms/ListFormWizardAppService.cs @@ -53,6 +53,7 @@ public class ListFormWizardAppService( IConfiguration configuration, LanguageTextAppService languageTextAppService, IDynamicDataManager dynamicDataManager, + IListFormSeedDataApplier seedDataApplier, ILogger logger ) : PlatformAppService(), IListFormWizardAppService { @@ -70,6 +71,7 @@ public class ListFormWizardAppService( private readonly IConfiguration _configuration = configuration; private readonly LanguageTextAppService _languageTextAppService = languageTextAppService; private readonly IDynamicDataManager _dynamicDataManager = dynamicDataManager; + private readonly IListFormSeedDataApplier _seedDataApplier = seedDataApplier; private readonly ILogger logger = logger; private readonly string cultureNameDefault = PlatformConsts.DefaultLanguage; @@ -534,6 +536,11 @@ public class ListFormWizardAppService( })]; } + /// + /// Kapsamdaki wizard seed dosyalarini listeler. Sira ilk deploy damgasina (SeededAt) + /// gore tersinedir — en son uretilen ekran listenin basinda durur; dosya adina gore siralamak + /// yeni eklenen ekrani listenin ortasinda birakiyordu. Damgasi okunamayan dosyalar sona duser. + /// public async Task> GetFiles() { var outputPath = ResolveWizardSeedOutputPath(); @@ -575,7 +582,10 @@ public class ListFormWizardAppService( } } - return result; + // CreatedAt sortable ("s") bicimindedir; metin karsilastirmasi da kronolojiktir. + return [.. result + .OrderByDescending(x => x.CreatedAt, StringComparer.Ordinal) + .ThenBy(x => x.FileName, StringComparer.OrdinalIgnoreCase)]; } public async Task GetFile(string fileName) @@ -804,8 +814,9 @@ public class ListFormWizardAppService( /// Zip icindeki yollar seed kapsam klasoru (host / tenants/{tenantId}) koku baz alinarak yazilir; /// dolayisiyla arsiv baska bir ortamda ayni klasor duzenine dogrudan acilabilir. /// - /// Custom yolunda: wizard/{dosya}.json, custom/{component}.json (bagimliliklari ile birlikte) - /// ve bu component'lerin data source'larinin isaret ettigi crud/{entity}.json dosyalari. + /// Custom yolunda: wizard/{dosya}.json, secili component ve bagimliliklariyla suzulmus + /// data/App.DeveloperKit.CustomComponents.json ve bu component'lerin data source'larinin + /// isaret ettigi crud/{entity}.json dosyalari. /// List yolunda: wizard/{dosya}.json, SelectCommand'a karsilik gelen /// {sql|postgres}/{object|execute}/{nesne}.sql ve varsa crud/{nesne}.json. /// @@ -828,8 +839,12 @@ public class ListFormWizardAppService( [$"{SeedPathResolver.WizardFolder}/{Path.GetFileName(filePath)}"] = filePath }; + // Paylasilan bir seed dosyasindan yalnizca ilgili satirlar suzuldugunde zip'e diskteki + // hali degil, uretilen icerik girer. + var generatedEntries = new Dictionary(StringComparer.OrdinalIgnoreCase); + if (seed.Wizard?.ComponentKind == WizardComponentKindEnum.Custom) - await CollectCustomComponentEntriesAsync(scopeRoot, seed.Wizard.CustomComponentName, entries); + await CollectCustomComponentEntriesAsync(scopeRoot, seed.Wizard.CustomComponentName, entries, generatedEntries); else CollectListComponentEntries(scopeRoot, seed.Wizard, entries); @@ -843,6 +858,13 @@ public class ListFormWizardAppService( await using var target = zipEntry.Open(); await source.CopyToAsync(target); } + + foreach (var entry in generatedEntries.OrderBy(x => x.Key, StringComparer.OrdinalIgnoreCase)) + { + var zipEntry = archive.CreateEntry(entry.Key, CompressionLevel.Optimal); + await using var target = zipEntry.Open(); + await target.WriteAsync(ImportEncoding.GetBytes(entry.Value)); + } } memory.Position = 0; @@ -869,6 +891,12 @@ public class ListFormWizardAppService( private const string ImportBackupFolder = "backup"; private const string ImportAppliedLogFile = "applied.log"; + /// Custom component'lerin seed kaynagi olan ListForm ekrani. + private const string CustomComponentsListFormCode = SeedConsts.AppCodes.DeveloperKits.CustomComponents; + + /// Custom component satirlarinin anahtar alani; bagimliliklar bu adla eslesir. + private const string CustomComponentNameField = "Name"; + private const long ImportMaxEntryBytes = 5 * 1024 * 1024; private const long ImportMaxArchiveBytes = 50 * 1024 * 1024; private const int ImportMaxEntryCount = 500; @@ -878,7 +906,7 @@ public class ListFormWizardAppService( [ SeedPathResolver.WizardFolder, SeedPathResolver.CrudFolder, - SeedPathResolver.CustomFolder, + SeedPathResolver.DataFolder, SeedPathResolver.SqlFolder, SeedPathResolver.PostgresFolder, ]; @@ -1003,6 +1031,24 @@ public class ListFormWizardAppService( var incomingText = NormalizeImportText(incoming); var existingText = NormalizeImportText(existing); + // Veri seed dosyasinda karsilastirma dosya degil satir bazlidir. + var scope = IsSeedDataPath(relativePath) ? BuildSeedDataScope(incoming, existing) : null; + if (scope != null) + { + incomingText = scope.Incoming; + existingText = scope.Existing; + entry.IncomingSize = ImportEncoding.GetByteCount(scope.Incoming); + entry.ExistingSize = ImportEncoding.GetByteCount(scope.Existing); + + // Gelen satirlarin hicbiri hedefte yoksa dosya var olsa da islem eklemedir. + if (!scope.HasExistingRows) + { + entry.Status = WizardImportEntryStatusEnum.New; + result.Entries.Add(entry); + continue; + } + } + if (string.Equals(incomingText, existingText, StringComparison.Ordinal)) { entry.Status = WizardImportEntryStatusEnum.Identical; @@ -1058,6 +1104,10 @@ public class ListFormWizardAppService( : ImportEncoding.GetBytes(input.Content); var overwritten = File.Exists(targetPath); + + if (overwritten && relativePath.StartsWith(SeedPathResolver.DataFolder + "/", StringComparison.OrdinalIgnoreCase)) + content = await MergeSeedDataRowsAsync(targetPath, content); + if (overwritten) { var backupPath = Path.Combine(sessionPath, ImportBackupFolder, systemPath); @@ -1087,12 +1137,106 @@ public class ListFormWizardAppService( }; } + /// + /// Import oturumunu kapatir. Kapanistan once zip'in getirdigi data/*.json dosyalari + /// veritabanina uygulanir: tabloda olmayan satirlar eklenir, var olanlar dosyadaki degerlerle + /// guncellenir ve soft delete edilmis satirlarin silinme izleri temizlenir — aksi halde kayit + /// tabloda durur ama ekranin varsayilan suzgeci onu gizlemeye devam ederdi. Hedefle zaten ayni + /// olan dosyalar da uygulanir: yazacak bir sey yoktur ama satirlar veritabaninda eksik ya da + /// silinmis olabilir. Migrate/seed akisindan farki budur — orada dosya bayat olabilir ve canli kayit + /// ezilmez; import ise kullanicinin bilincli getirdigi icerigi tasir. Diger klasorler + /// (wizard, crud, sql) dosya olarak kalir; onlar seeder ya da wizard deploy ile veritabanina gecer. + /// + /// Uygulama basarisiz olsa bile oturum kapatilir; dosyalar hedefe yazilmis durumdadir ve + /// hata sonuca tasinir. Bu adim geri alinamaz, cunku dosya yedegi veritabani kaydini + /// kapsamaz — geri alma RollbackImport ile bu adimdan once yapilir. + /// + /// [Authorize(AppCodes.Listforms.Wizards.Import)] [HttpPost("api/app/list-form-wizard/complete-import")] - public Task CompleteImport(string importId) + public async Task CompleteImport(string importId) { - DeleteImportSession(GetExistingImportSessionPath(importId)); - return Task.CompletedTask; + var sessionPath = GetExistingImportSessionPath(importId); + var scopeRoot = Path.GetFullPath(SeedPathResolver.GetScopePath(_configuration, CurrentTenant.Id)); + var result = new WizardImportCompleteResultDto(); + + var dataFiles = await ReadAppliedDataFilesAsync(sessionPath, scopeRoot); + if (dataFiles.Count > 0) + { + foreach (var applied in await _seedDataApplier.ApplyFilesAsync(CurrentTenant.Id, dataFiles, overwriteExisting: true)) + { + result.AppliedData.Add(new WizardImportSeedApplyResultDto + { + Path = applied.Path, + InsertedRowCount = applied.Inserted, + UpdatedRowCount = applied.Updated, + SkippedRowCount = applied.Skipped, + Error = applied.Error, + }); + } + } + + DeleteImportSession(sessionPath); + return result; + } + + /// + /// Veritabanina uygulanacak data/ girdilerini toplar. + /// + /// Once oturumda gercekten yazilmis dosyalar (kayit dosyasindan) alinir. Ardindan yazilmamis + /// staging girdilerinden hedefle ayni olanlar eklenir: dosya ayni oldugu icin yazacak + /// bir sey yoktu, ama satirlar veritabaninda eksik ya da soft delete edilmis olabilir. Hedeften + /// farkli olup yazilmamis dosya kullanicinin "mevcudu koru" karari demektir; ona dokunulmaz. + /// + /// + private async Task> ReadAppliedDataFilesAsync(string sessionPath, string scopeRoot) + { + var prefix = SeedPathResolver.DataFolder + "/"; + var paths = new List(); + var seen = new HashSet(StringComparer.OrdinalIgnoreCase); + + var logPath = Path.Combine(sessionPath, ImportAppliedLogFile); + if (File.Exists(logPath)) + { + foreach (var line in await File.ReadAllLinesAsync(logPath)) + { + var parts = line.Split('|', 2); + if (parts.Length != 2 || !parts[1].StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + continue; + + if (seen.Add(parts[1])) + paths.Add(parts[1]); + } + } + + var stagedDataPath = Path.Combine(sessionPath, ImportStagedFolder, SeedPathResolver.DataFolder); + if (!Directory.Exists(stagedDataPath)) + return paths; + + foreach (var stagedFile in Directory.GetFiles(stagedDataPath, "*.json")) + { + var relativePath = prefix + Path.GetFileName(stagedFile); + if (seen.Contains(relativePath)) + continue; + + var targetPath = ResolveScopeFilePath(scopeRoot, relativePath); + if (targetPath == null) + continue; + + var staged = await File.ReadAllBytesAsync(stagedFile); + var existing = await File.ReadAllBytesAsync(targetPath); + + // Karsilastirma analiz adimiyla ayni olmali: veri dosyalarinda satir bazlidir. + var scope = BuildSeedDataScope(staged, existing); + var isIdentical = scope != null + ? string.Equals(scope.Incoming, scope.Existing, StringComparison.Ordinal) + : string.Equals(NormalizeImportText(staged), NormalizeImportText(existing), StringComparison.Ordinal); + + if (isIdentical && seen.Add(relativePath)) + paths.Add(relativePath); + } + + return paths; } [Authorize(AppCodes.Listforms.Wizards.Import)] @@ -1143,6 +1287,156 @@ public class ListFormWizardAppService( DeleteImportSession(sessionPath); } + /// Kapsam-goreli yolun bir veri seed dosyasini gosterip gostermedigi. + private static bool IsSeedDataPath(string relativePath) + { + return relativePath.StartsWith(SeedPathResolver.DataFolder + "/", StringComparison.OrdinalIgnoreCase); + } + + /// + /// Veri seed dosyasinin karsilastirilacak kisimlari. hedef dosyadan + /// yalnizca gelen satirlarin anahtarlariyla suzulur; false ise + /// gelen satirlarin hicbiri hedefte yoktur. + /// + private sealed record SeedDataScope(string Incoming, string Existing, bool HasExistingRows); + + /// + /// Diff'te gosterilen suzulmus veri seed icerigi. Dosya duzeyindeki GeneratedAt ve + /// Order disarida birakilir; birlestirme bu alanlari hedeften korudugu icin + /// karsilastirmaya girmeleri sahte fark uretirdi. + /// + private sealed class ListFormSeedDataScopeDto + { + public string ListFormCode { get; set; } = string.Empty; + + public string KeyFieldName { get; set; } = string.Empty; + + public List> Rows { get; set; } = []; + } + + /// + /// Bir veri seed dosyasi kapsamdaki butun satirlari tasir, zip ise yalnizca aktarilan ekranin + /// satirlarini. Diff'te dosyanin tamami karsilastirilirsa kullanici ilgisiz kayitlarin farkini + /// gorur; bu yuzden iki taraf da gelen satirlarin anahtarlarina indirgenir. Dosya duzeyindeki + /// alanlar (GeneratedAt, Order) karsilastirmaya girmez — birlestirme onlari zaten + /// hedeften korur. Dosyalardan biri cozulemiyorsa null doner ve dosyanin tamami karsilastirilir. + /// + private static SeedDataScope? BuildSeedDataScope(byte[] incoming, byte[] existing) + { + try + { + var incomingFile = JsonSerializer.Deserialize(incoming, SeedJsonReadOptions); + var existingFile = JsonSerializer.Deserialize(existing, SeedJsonReadOptions); + + var keyFieldName = incomingFile?.KeyFieldName ?? existingFile?.KeyFieldName; + if (incomingFile == null || existingFile == null || string.IsNullOrWhiteSpace(keyFieldName)) + return null; + + var existingByKey = new Dictionary>(StringComparer.OrdinalIgnoreCase); + foreach (var row in existingFile.Rows) + { + var key = GetRowText(row, keyFieldName); + if (!string.IsNullOrWhiteSpace(key)) + existingByKey[key] = row; + } + + // Hedef satirlar gelen sirayla dizilir; sira farki diff'te sahte fark uretmesin. + var matched = new List>(); + foreach (var row in incomingFile.Rows) + { + var key = GetRowText(row, keyFieldName); + if (!string.IsNullOrWhiteSpace(key) && existingByKey.TryGetValue(key, out var match)) + matched.Add(match); + } + + return new SeedDataScope( + SerializeSeedDataScope(incomingFile, keyFieldName, incomingFile.Rows), + SerializeSeedDataScope(existingFile, keyFieldName, matched), + matched.Count > 0); + } + catch (JsonException) + { + return null; + } + } + + /// + /// Suzulmus satirlari, birlestirmenin okudugu alanlarla (ekran kodu, anahtar alan, satirlar) + /// yeniden yazar. Kullanici diff ekraninda bu icerigi duzenleyip gonderdiginde de + /// ayni sozlesmeyi bulur. + /// + private static string SerializeSeedDataScope( + ListFormSeedDataFileDto source, + string keyFieldName, + List> rows) + { + return JsonSerializer.Serialize( + new ListFormSeedDataScopeDto + { + ListFormCode = source.ListFormCode, + KeyFieldName = keyFieldName, + Rows = rows, + }, + ListFormSeedDataFileDto.JsonOptions); + } + + /// + /// Bir veri seed dosyasi kapsamdaki butun satirlari tasir, zip ise yalnizca aktarilan ekranin + /// satirlarini getirir; bu yuzden hedef dosyanin ustune yazilmaz, satirlar anahtar alan + /// uzerinden birlestirilir: gelen satir hedefte varsa guncellenir, yoksa sona eklenir, + /// hedefteki diger satirlar korunur. Iki dosyadan biri cozulemiyorsa ya da anahtar alan + /// bilinmiyorsa gelen icerik oldugu gibi yazilir. + /// + private async Task MergeSeedDataRowsAsync(string targetPath, byte[] incoming) + { + try + { + var incomingFile = JsonSerializer.Deserialize(incoming, SeedJsonReadOptions); + var existingFile = JsonSerializer.Deserialize( + await File.ReadAllBytesAsync(targetPath), SeedJsonReadOptions); + + var keyFieldName = incomingFile?.KeyFieldName ?? existingFile?.KeyFieldName; + if (incomingFile == null || existingFile == null || string.IsNullOrWhiteSpace(keyFieldName)) + return incoming; + + var merged = new List>(existingFile.Rows); + var indexByKey = new Dictionary(StringComparer.OrdinalIgnoreCase); + for (var i = 0; i < merged.Count; i++) + { + var key = GetRowText(merged[i], keyFieldName); + if (!string.IsNullOrWhiteSpace(key)) + indexByKey[key] = i; + } + + foreach (var row in incomingFile.Rows) + { + var key = GetRowText(row, keyFieldName); + if (string.IsNullOrWhiteSpace(key)) + continue; + + if (indexByKey.TryGetValue(key, out var index)) + { + merged[index] = row; + } + else + { + indexByKey[key] = merged.Count; + merged.Add(row); + } + } + + existingFile.Rows = merged; + existingFile.GeneratedAt = Clock.Now.ToUniversalTime(); + + return ImportEncoding.GetBytes(JsonSerializer.Serialize(existingFile, ListFormSeedDataFileDto.JsonOptions)); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Seed data file could not be merged, incoming content is written as is: {TargetPath}", targetPath); + return incoming; + } + } + /// /// Zip girdisini kapsam klasorune goreli yola cevirir. Zip, kapsam klasorunun icinden /// (wizard/x.json) ya da depo kokunden (configs/seeds/host/wizard/x.json) @@ -1255,63 +1549,120 @@ public class ListFormWizardAppService( } /// - /// Custom component seed dosyasini, bagimli oldugu component'leri ve bunlarin data source'larinin - /// isaret ettigi crud seed dosyalarini toplar. Bagimlilik zinciri dongu icerse bile - /// ziyaret edilen adlar takip edildigi icin sonsuz dongu olusmaz. + /// Custom component'lerin seed kaynagi data/App.DeveloperKit.CustomComponents.json + /// dosyasidir. Dosya kapsamdaki butun component'leri tasidigi icin zip'e oldugu gibi degil, + /// secili component ile bagimlilik zincirine suzulmus bir kopyasi konur; boylece arsiv ilgisiz + /// component'leri disari tasimaz. Ayni zincirdeki component'lerin data source'larinin isaret + /// ettigi crud seed dosyalari da toplanir. Bagimlilik zinciri dongu icerse bile ziyaret edilen + /// adlar takip edildigi icin sonsuz dongu olusmaz. /// - private async Task CollectCustomComponentEntriesAsync(string scopeRoot, string? componentName, Dictionary entries) + private async Task CollectCustomComponentEntriesAsync( + string scopeRoot, + string? componentName, + Dictionary entries, + Dictionary generatedEntries) { + if (string.IsNullOrWhiteSpace(componentName)) + return; + + var relativePath = SeedPathResolver.GetDataFilePath(CustomComponentsListFormCode); + var sourcePath = ResolveScopeFilePath(scopeRoot, relativePath); + if (sourcePath == null) + { + logger.LogWarning("Custom component seed data file not found for export: {RelativePath}", relativePath); + return; + } + + ListFormSeedDataFileDto? seedFile; + try + { + seedFile = JsonSerializer.Deserialize( + await File.ReadAllTextAsync(sourcePath), SeedJsonReadOptions); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Custom component seed data file could not be parsed for export: {RelativePath}", relativePath); + return; + } + + if (seedFile == null) + return; + + var rowsByName = new Dictionary>(StringComparer.OrdinalIgnoreCase); + foreach (var row in seedFile.Rows) + { + var name = GetRowText(row, CustomComponentNameField); + if (!string.IsNullOrWhiteSpace(name)) + rowsByName[name] = row; + } + var pending = new Queue(); var visited = new HashSet(StringComparer.OrdinalIgnoreCase); - - if (!string.IsNullOrWhiteSpace(componentName)) - pending.Enqueue(componentName.Trim()); + var exported = new HashSet(StringComparer.OrdinalIgnoreCase); + pending.Enqueue(componentName.Trim()); while (pending.Count > 0) { var name = pending.Dequeue(); - if (!visited.Add(name)) + if (!visited.Add(name) || !rowsByName.TryGetValue(name, out var row)) continue; - var relativePath = $"{SeedPathResolver.CustomFolder}/{name}.json"; - if (!TryAddSeedEntry(scopeRoot, relativePath, entries)) + exported.Add(name); + + foreach (var dependency in ParseSeedRowList(GetRowText(row, "Dependencies"))) { - logger.LogWarning("Custom component seed file not found for export: {RelativePath}", relativePath); - continue; + if (!string.IsNullOrWhiteSpace(dependency)) + pending.Enqueue(dependency.Trim()); } - CustomComponentSeedFileDto? customSeed; - try + foreach (var dataSource in ParseSeedRowList(GetRowText(row, "DataSources"))) { - var customJson = await File.ReadAllTextAsync(entries[relativePath]); - customSeed = JsonSerializer.Deserialize(customJson, SeedJsonReadOptions); - } - catch (Exception ex) - { - logger.LogWarning(ex, "Custom component seed file could not be parsed for export: {RelativePath}", relativePath); - continue; - } + // SeedFile yalnizca CRUD Endpoint Manager'in urettigi endpointlerde doludur; + // elle yazilmis bir URL'in seed karsiligi yoktur. + if (string.IsNullOrWhiteSpace(dataSource.SeedFile)) + continue; - foreach (var component in customSeed?.CustomComponents ?? []) - { - foreach (var dependency in component.Dependencies) - { - if (!string.IsNullOrWhiteSpace(dependency)) - pending.Enqueue(dependency.Trim()); - } - - foreach (var dataSource in component.DataSources) - { - // SeedFile yalnizca CRUD Endpoint Manager'in urettigi endpointlerde doludur; - // elle yazilmis bir URL'in seed karsiligi yoktur. - if (string.IsNullOrWhiteSpace(dataSource.SeedFile)) - continue; - - if (!TryAddSeedEntry(scopeRoot, dataSource.SeedFile, entries)) - logger.LogWarning("CRUD seed file not found for export: {RelativePath}", dataSource.SeedFile); - } + if (!TryAddSeedEntry(scopeRoot, dataSource.SeedFile, entries)) + logger.LogWarning("CRUD seed file not found for export: {RelativePath}", dataSource.SeedFile); } } + + if (exported.Count == 0) + { + logger.LogWarning("Custom component not found in the seed data file for export: {ComponentName}", componentName); + return; + } + + // Dosyadaki satir sirasi korunur; hedefte satirlar bu sirayla birlestirilir. + seedFile.Rows = [.. seedFile.Rows.Where(x => exported.Contains(GetRowText(x, CustomComponentNameField) ?? string.Empty))]; + generatedEntries[relativePath] = JsonSerializer.Serialize(seedFile, ListFormSeedDataFileDto.JsonOptions); + } + + /// Seed satirindaki bir alani metin olarak okur; JsonElement ve duz deger destekler. + private static string? GetRowText(Dictionary row, string fieldName) + { + if (!row.TryGetValue(fieldName, out var value) || value is null) + return null; + + return value is JsonElement element + ? (element.ValueKind == JsonValueKind.String ? element.GetString() : element.ToString()) + : value.ToString(); + } + + /// Seed satirinda JSON dizisi olarak tutulan alanlari cozer; bozuk deger bos liste doner. + private static List ParseSeedRowList(string? json) + { + if (string.IsNullOrWhiteSpace(json)) + return []; + + try + { + return JsonSerializer.Deserialize>(json, SeedJsonReadOptions) ?? []; + } + catch (JsonException) + { + return []; + } } /// @@ -1369,20 +1720,30 @@ public class ListFormWizardAppService( /// sessizce reddedilir. Dosya yoksa false doner. /// private static bool TryAddSeedEntry(string scopeRoot, string relativePath, Dictionary entries) + { + var fullPath = ResolveScopeFilePath(scopeRoot, relativePath); + if (fullPath == null) + return false; + + entries[relativePath.Replace('\\', '/').Trim('/')] = fullPath; + return true; + } + + /// + /// Kapsam klasorune goreli yolu var olan bir dosyanin tam yoluna cevirir; kapsam disina cikan + /// ya da bulunmayan bir yol icin null doner. + /// + private static string? ResolveScopeFilePath(string scopeRoot, string relativePath) { var normalized = relativePath.Replace('\\', '/').Trim('/'); if (normalized.Length == 0 || normalized.Contains("..", StringComparison.Ordinal)) - return false; + return null; var fullPath = Path.GetFullPath(Path.Combine(scopeRoot, normalized.Replace('/', Path.DirectorySeparatorChar))); if (!fullPath.StartsWith(scopeRoot + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase)) - return false; + return null; - if (!File.Exists(fullPath)) - return false; - - entries[normalized] = fullPath; - return true; + return File.Exists(fullPath) ? fullPath : null; } private async Task> GetTableColumnNamesAsync(string dataSourceCode, SelectCommandTypeEnum commandType, string selectCommand) diff --git a/api/src/Sozsoft.Platform.DbMigrator/Migrations/CustomComponentDataSeeder.cs b/api/src/Sozsoft.Platform.DbMigrator/Migrations/CustomComponentDataSeeder.cs deleted file mode 100644 index 5c7fe7f1..00000000 --- a/api/src/Sozsoft.Platform.DbMigrator/Migrations/CustomComponentDataSeeder.cs +++ /dev/null @@ -1,206 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text.Json; -using System.Threading.Tasks; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; -using Sozsoft.Platform.Entities; -using Volo.Abp.Data; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Domain.Repositories; - -namespace Sozsoft.Platform.Data.Seeds; - -/// -/// Component Manager ile olusturulan custom component'leri -/// {host|tenants/{tenantId}}/custom/*.json dosyalarindan okuyarak veritabanina uygular -/// (kapsam, seed edilen veritabanini izler). -/// -/// Dosyalar component kaydedildikce/silindikce otomatik yazildigi icin kaynak kabul edilir: -/// ayni adli component varsa dosyadaki icerikle guncellenir, yoksa olusturulur. -/// Dosyada olmayan component'ler silinmez. -/// -/// -public class CustomComponentDataSeeder : IDataSeedContributor, ITransientDependency -{ - private static readonly JsonSerializerOptions JsonReadOptions = new() - { - PropertyNameCaseInsensitive = true - }; - - private static readonly JsonSerializerOptions JsonWriteOptions = new() - { - WriteIndented = true - }; - - private readonly IRepository _repository; - private readonly IConfiguration _configuration; - private readonly ILogger _logger; - - public CustomComponentDataSeeder( - IRepository repository, - IConfiguration configuration, - ILogger logger) - { - _repository = repository; - _configuration = configuration; - _logger = logger; - } - - public async Task SeedAsync(DataSeedContext context) - { - var scopeFolderName = SeedPathResolver.GetScopeFolderName(context.TenantId); - var customDataPath = SeedPathResolver.GetScopePath(_configuration, context.TenantId, SeedPathResolver.CustomFolder); - - if (!Directory.Exists(customDataPath)) - { - _logger.LogInformation("Seeds/{Scope}/custom directory not found, skipping CustomComponentDataSeeder.", scopeFolderName); - return; - } - - var jsonFiles = Directory.GetFiles(customDataPath, "*.json").OrderBy(Path.GetFileName).ToArray(); - if (jsonFiles.Length == 0) - { - _logger.LogInformation("No JSON files found in Seeds/{Scope}/custom directory, skipping CustomComponentDataSeeder.", scopeFolderName); - return; - } - - // Eski seed dosyalarinda DataSources blogu yoktur; katalog, bloksuz dosyalarda - // endpoint listesinin Props'tan turetilebilmesi icin bastan okunur. - var crudEndpoints = ReadCrudEndpointCatalog(context.TenantId, scopeFolderName); - - _logger.LogInformation("CustomComponentDataSeeder started. {Count} file(s) to be processed.", jsonFiles.Length); - - foreach (var filePath in jsonFiles) - { - var fileName = Path.GetFileName(filePath); - try - { - var json = await File.ReadAllTextAsync(filePath); - var seedFile = JsonSerializer.Deserialize(json, JsonReadOptions); - - if (seedFile?.CustomComponents == null || seedFile.CustomComponents.Count == 0) - { - _logger.LogWarning("[{File}] No custom component found, skipped.", fileName); - continue; - } - - foreach (var item in seedFile.CustomComponents) - { - await ApplyAsync(fileName, item, crudEndpoints); - } - } - catch (Exception ex) - { - // Tek bir dosyanin hatasi diger component'lerin seed edilmesini engellemez. - _logger.LogError(ex, "Failed to process custom component seed file: {FileName}", fileName); - } - } - - _logger.LogInformation("CustomComponentDataSeeder completed. {Count} file(s) processed.", jsonFiles.Length); - } - - /// - /// Kapsamdaki crud/*.json dosyalarini okuyarak endpoint katalogunu olusturur. - /// Veritabani yerine dosyalardan okunur; boylece seed sirasi CrudEndpointDataSeeder'a bagli kalmaz. - /// - private List ReadCrudEndpointCatalog( - Guid? tenantId, - string scopeFolderName) - { - var references = new List(); - var crudDataPath = SeedPathResolver.GetScopePath(_configuration, tenantId, SeedPathResolver.CrudFolder); - - if (!Directory.Exists(crudDataPath)) - { - return references; - } - - foreach (var filePath in Directory.GetFiles(crudDataPath, "*.json")) - { - try - { - var seedFile = JsonSerializer.Deserialize( - File.ReadAllText(filePath), - JsonReadOptions); - - if (seedFile?.Endpoints == null) - { - continue; - } - - references.AddRange(seedFile.Endpoints.Select(endpoint => - new CustomComponentDataSourceResolver.CrudEndpointReference - { - EntityName = seedFile.EntityName, - Method = endpoint.Method, - Path = endpoint.Path, - OperationType = endpoint.OperationType - })); - } - catch (Exception ex) - { - // Katalog yardimci bilgidir; okunamayan bir dosya component seed'ini durdurmaz. - _logger.LogWarning(ex, "[{Scope}] CRUD seed file could not be read for the data source catalog: {File}", - scopeFolderName, Path.GetFileName(filePath)); - } - } - - return references; - } - - private async Task ApplyAsync( - string fileName, - CustomComponentSeedItemDto item, - List crudEndpoints) - { - var name = item.Name?.Trim(); - if (string.IsNullOrWhiteSpace(name)) - { - _logger.LogWarning("[{File}] Component name is empty, skipped.", fileName); - return; - } - - var dependencies = JsonSerializer.Serialize(item.Dependencies ?? new List()); - // DataSources blogu olmayan (bu sutundan once yazilmis) dosyalarda liste Props'taki - // designer dokumanindan turetilir; boylece sutun her durumda dolar. - var dataSourceItems = item.DataSources is { Count: > 0 } - ? item.DataSources - : CustomComponentDataSourceResolver.Resolve(item.Props, crudEndpoints); - - // Bos liste sutunu doldurmaya degmez; component'in hic endpoint'i yoksa null kalir. - var dataSources = dataSourceItems.Count > 0 - ? JsonSerializer.Serialize(dataSourceItems, JsonWriteOptions) - : null; - var current = await _repository.FirstOrDefaultAsync(x => x.Name == name); - - if (current == null) - { - await _repository.InsertAsync(new CustomComponent( - name, - item.RoutePath, - item.Code, - item.Props, - item.Description, - item.IsActive, - dependencies, - dataSources), autoSave: true); - - _logger.LogInformation("[{File}] '{Name}' created.", fileName, name); - return; - } - - current.RoutePath = item.RoutePath; - current.Code = item.Code; - current.Props = item.Props; - current.Description = item.Description; - current.IsActive = item.IsActive; - current.Dependencies = dependencies; - current.DataSources = dataSources; - - await _repository.UpdateAsync(current, autoSave: true); - _logger.LogInformation("[{File}] '{Name}' updated.", fileName, name); - } -} diff --git a/api/src/Sozsoft.Platform.DbMigrator/Migrations/LanguagesData.json b/api/src/Sozsoft.Platform.DbMigrator/Migrations/LanguagesData.json index a4414d2d..afc13b40 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Migrations/LanguagesData.json +++ b/api/src/Sozsoft.Platform.DbMigrator/Migrations/LanguagesData.json @@ -732,12 +732,6 @@ "en": "Crud Endpoints", "tr": "Crud Uç Noktaları" }, - { - "resourceName": "Platform", - "key": "App.DeveloperKit.Components", - "en": "Custom Components", - "tr": "Özel Bileşenler" - }, { "resourceName": "Platform", "key": "App.DeveloperKit.DynamicServices", @@ -9026,15 +9020,21 @@ }, { "resourceName": "Platform", - "key": "App.DeveloperKitComponent.ComponentDescription", - "en": "Manage your custom React components", - "tr": "Özel React bileşenlerinizi yönetin" + "key": "App.Listform.ListformField.DataSources", + "en": "Data Sources", + "tr": "Veri Kaynakları" }, { "resourceName": "Platform", - "key": "App.DeveloperKitComponent.New", - "en": "New Component", - "tr": "Yeni Bileşen" + "key": "App.DeveloperKit.CustomComponents", + "en": "Custom Components", + "tr": "Özel Bileşenler" + }, + { + "resourceName": "Platform", + "key": "App.DeveloperKit.CustomComponents.Description", + "en": "Custom components built with the Visual Designer", + "tr": "Visual Designer ile üretilen özel bileşenler" }, { "resourceName": "Platform", @@ -9072,24 +9072,12 @@ "en": "Total number of inactive custom React components", "tr": "Pasif özel React bileşenlerinin toplam sayısı" }, - { - "resourceName": "Platform", - "key": "App.DeveloperKitComponent.SearchPlaceholder", - "en": "Search components...", - "tr": "Bileşenlerde ara..." - }, { "resourceName": "Platform", "key": "App.ComponentFilter.All", "en": "All Components", "tr": "Tüm Bileşenler" }, - { - "resourceName": "Platform", - "key": "App.DeveloperKitComponent.NoDependencies", - "en": "No dependencies", - "tr": "Bağımlılık yok" - }, { "resourceName": "Platform", "key": "App.DeveloperKitComponent.SaveToSeed", @@ -9132,12 +9120,6 @@ "en": "Delete Component", "tr": "Bileşeni Sil" }, - { - "resourceName": "Platform", - "key": "App.DeveloperKitComponent.ConfirmDelete", - "en": "Are you sure you want to delete the component?", - "tr": "Bileşeni silmek istediğinizden emin misiniz?" - }, { "resourceName": "Platform", "key": "App.EmptyFiltered.FilteredTitle", @@ -16455,8 +16437,8 @@ { "resourceName": "Platform", "key": "App.ListForm.WizardFileExport", - "en": "Export as zip (wizard, custom/crud and sql seed files)", - "tr": "Zip olarak dışa aktar (wizard, custom/crud ve sql seed dosyaları)" + "en": "Export as zip (wizard, data/crud and sql seed files)", + "tr": "Zip olarak dışa aktar (wizard, data/crud ve sql seed dosyaları)" }, { "resourceName": "Platform", @@ -27231,8 +27213,26 @@ { "resourceName": "Platform", "key": "App.ListForm.WizardImportRunMigrateHint", - "en": "Run DB Migrate to load the imported seed files into the database.", - "tr": "İçe aktarılan seed dosyalarının veritabanına işlenmesi için DB Migrate çalıştırın." + "en": "Data rows were written to the database. Run DB Migrate for the remaining seed files (wizard, crud, sql).", + "tr": "Veri satırları veritabanına yazıldı. Kalan seed dosyaları (wizard, crud, sql) için DB Migrate çalıştırın." + }, + { + "resourceName": "Platform", + "key": "App.ListForm.WizardImportDataApplied", + "en": "Rows written to the database", + "tr": "Veritabanına yazılan satırlar" + }, + { + "resourceName": "Platform", + "key": "App.ListForm.WizardImportDataRowSummary", + "en": "{inserted} inserted, {updated} updated, {skipped} unchanged", + "tr": "{inserted} eklendi, {updated} güncellendi, {skipped} değişmedi" + }, + { + "resourceName": "Platform", + "key": "App.ListForm.WizardImportDataApplyFailed", + "en": "Files were written but some rows could not be applied to the database.", + "tr": "Dosyalar yazıldı ancak bazı satırlar veritabanına uygulanamadı." }, { "resourceName": "Platform", diff --git a/api/src/Sozsoft.Platform.DbMigrator/Migrations/ListFormSeeder_Saas.cs b/api/src/Sozsoft.Platform.DbMigrator/Migrations/ListFormSeeder_Saas.cs index 5ead59b3..929a1534 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Migrations/ListFormSeeder_Saas.cs +++ b/api/src/Sozsoft.Platform.DbMigrator/Migrations/ListFormSeeder_Saas.cs @@ -6897,9 +6897,9 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency SelectCommandType = SelectCommandTypeEnum.Table, SelectCommand = TableNameResolver.GetFullTableName(nameof(TableNameEnum.CustomEndpoint)), SeedFilePath = SeedPathResolver.GetDataFilePath(listFormName), - SeedSyncInsert = false, - SeedSyncUpdate = false, - SeedSyncDelete = false, + SeedSyncInsert = true, + SeedSyncUpdate = true, + SeedSyncDelete = true, KeyFieldName = "Id", KeyFieldDbSourceType = DbType.Guid, DefaultFilter = DefaultFilterJson, @@ -7179,9 +7179,9 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency SelectCommandType = SelectCommandTypeEnum.Table, SelectCommand = dynamicServiceTable, SeedFilePath = SeedPathResolver.GetDataFilePath(listFormName), - SeedSyncInsert = false, - SeedSyncUpdate = false, - SeedSyncDelete = false, + SeedSyncInsert = true, + SeedSyncUpdate = true, + SeedSyncDelete = true, KeyFieldName = "Id", KeyFieldDbSourceType = DbType.Guid, DefaultFilter = DefaultFilterJson, @@ -7527,6 +7527,322 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency } #endregion + #region Custom Components + listFormName = AppCodes.DeveloperKits.CustomComponents; + if (!await _listFormRepository.AnyAsync(a => a.ListFormCode == listFormName)) + { + // Ekran bilesenin ust bilgilerini yonetir; kod ve tasarim Visual Designer sayfasindadir. + var customComponentTable = TableNameResolver.GetFullTableName(nameof(TableNameEnum.CustomComponent)); + + var listForm = await _listFormRepository.InsertAsync( + new ListForm() + { + ListFormType = ListFormTypeEnum.List, + PageSize = 10, + ExportJson = DefaultExportJson, + IsSubForm = false, + ShowNote = false, + LayoutJson = JsonSerializer.Serialize(new LayoutDto + { + Grid = true, + Card = true, + Pivot = false, + Chart = false, + Tree = false, + Gantt = false, + Scheduler = false, + Todo = false, + DefaultLayout = "grid", + }), + CultureName = LanguageCodes.En, + ListFormCode = listFormName, + Name = listFormName, + Title = listFormName, + DataSourceCode = SeedConsts.DataSources.DefaultCode, + IsTenant = false, + IsBranch = false, + IsOrganizationUnit = false, + Description = $"{listFormName}.Description", + SelectCommandType = SelectCommandTypeEnum.Table, + SelectCommand = customComponentTable, + SeedFilePath = SeedPathResolver.GetDataFilePath(listFormName), + SeedSyncInsert = true, + SeedSyncUpdate = true, + SeedSyncDelete = true, + KeyFieldName = "Id", + KeyFieldDbSourceType = DbType.Guid, + DefaultFilter = DefaultFilterJson, + SortMode = GridOptions.SortModeSingle, + FilterRowJson = DefaultFilterRowJson(), + HeaderFilterJson = DefaultHeaderFilterJson(), + SearchPanelJson = DefaultSearchPanelJson(), + GroupPanelJson = DefaultGroupPanelJson(), + SelectionJson = DefaultSelectionSingleJson, + ColumnOptionJson = DefaultColumnOptionJson(), + PermissionJson = DefaultPermissionJson(listFormName), + PagerOptionJson = DefaultPagerOptionJson, + // Ekleme/duzenleme grid'in kendi popup formunda; kod tarafi Design butonundan acilir. + EditingOptionJson = DefaultEditingOptionJson(listFormName, 700, 520, true, true, true, true, false), + EditingFormJson = JsonSerializer.Serialize(new List() + { + new() { Order = 1, ColCount = 2, ColSpan = 1, ItemType = "group" }, + }), + InsertFieldsDefaultValueJson = JsonSerializer.Serialize(new FieldsDefaultValue[] { + new() { FieldName = "CreationTime", FieldDbType = DbType.DateTimeOffset, Value = "@NOW", CustomValueType = FieldCustomValueTypeEnum.CustomKey }, + new() { FieldName = "CreatorId", FieldDbType = DbType.Guid, Value = "@USERID", CustomValueType = FieldCustomValueTypeEnum.CustomKey }, + new() { FieldName = "Id", FieldDbType = DbType.Guid, Value = "@NEWID", CustomValueType = FieldCustomValueTypeEnum.CustomKey }, + // Kod ve tasarim dokumani tasarimcida uretilir; kolon NOT NULL oldugu icin bos baslar. + new() { FieldName = "Code", FieldDbType = DbType.String, Value = "", CustomValueType = FieldCustomValueTypeEnum.Value }, + }), + FormFieldsDefaultValueJson = JsonSerializer.Serialize(new FieldsDefaultValue[] { + new() { FieldName = "IsActive", FieldDbType = DbType.Boolean, Value = "true", CustomValueType = FieldCustomValueTypeEnum.Value }, + }), + DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.CustomComponent)), + DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(), + CommandColumnJson = JsonSerializer.Serialize(new CommandColumnDto[] { + new() { + ButtonPosition = UiCommandButtonPositionTypeEnum.CommandColumn, + Hint = "App.Platform.Design", + Text = "App.Platform.Design", + AuthName = listFormName + ".Update", + Url = "/admin/developerkit/components/design/@Id", + UrlTarget = "_blank", + IsVisible = true, + }, + new() { + ButtonPosition = UiCommandButtonPositionTypeEnum.CommandColumn, + Hint = "App.Platform.OpenUrl", + Text = "App.Platform.OpenUrl", + AuthName = listFormName, + Url = "@RoutePath", + UrlTarget = "_blank", + // Pasif ya da rotasi olmayan bilesenin onizlemesi acilmaz. + VisibleExpression = "(e) => Boolean(e.row.data.IsActive) && Boolean(e.row.data.RoutePath)", + IsVisible = true, + }, + }), + WidgetsJson = JsonSerializer.Serialize(new List + { + new() + { + ColGap = 3, + ColSpan = 4, + SqlQuery = $@" + SELECT + 'App.DeveloperKitComponent.Total' AS ""Title"", + COUNT(*) AS ""Value"", + 'purple' AS ""Color"", + 'App.DeveloperKitComponent.TotalDescription' AS ""SubTitle"", + 'FaPuzzlePiece' AS ""Icon"" + FROM ""{customComponentTable}"" + WHERE ""IsDeleted"" = 'false' + + UNION ALL + + SELECT + 'App.DeveloperKitComponent.Active' AS ""Title"", + COUNT(*) AS ""Value"", + 'green' AS ""Color"", + 'App.DeveloperKitComponent.ActiveDescription' AS ""SubTitle"", + 'FaCheckCircle' AS ""Icon"" + FROM ""{customComponentTable}"" + WHERE ""IsDeleted"" = 'false' AND ""IsActive"" = 'true' + + UNION ALL + + SELECT + 'App.DeveloperKitComponent.Inactive' AS ""Title"", + COUNT(*) AS ""Value"", + 'gray' AS ""Color"", + 'App.DeveloperKitComponent.InactiveDescription' AS ""SubTitle"", + 'FaTimesCircle' AS ""Icon"" + FROM ""{customComponentTable}"" + WHERE ""IsDeleted"" = 'false' AND ""IsActive"" = 'false' + ", + Title = "Title", + Value = "Value", + ValueClassName = "text-3xl", + Color = "Color", + Icon = "Icon", + SubTitle = "SubTitle", + ClassName = "mb-3", + IsActive = true + } + }), + } + ); + + #region Custom Components Fields + await _listFormFieldRepository.InsertManyAsync([ + new() { + ListFormCode = listForm.ListFormCode, + CultureName = LanguageCodes.En, + SourceDbType = DbType.Guid, + FieldName = "Id", + CaptionName = "App.Listform.ListformField.Id", + Width = 0, + ListOrderNo = 1, + Visible = false, + IsActive = true, + + ColumnCustomizationJson = DefaultColumnCustomizationJson, + PermissionJson = DefaultFieldPermissionJson(listForm.Name), + PivotSettingsJson = DefaultPivotSettingsJson + }, + new() { + ListFormCode = listForm.ListFormCode, + CultureName = LanguageCodes.En, + SourceDbType = DbType.String, + FieldName = "Name", + EditGroupOrderNo = 1, + EditOrderNo = 1, + EditorType2 = EditorTypes.dxTextBox, + ColSpan = 1, + CaptionName = "App.Listform.ListformField.Name", + Width = 0, + ListOrderNo = 2, + Visible = true, + IsActive = true, + + AllowSearch = true, + ValidationRuleJson = DefaultValidationRuleRequiredJson, + ColumnCustomizationJson = DefaultColumnCustomizationJson, + PermissionJson = DefaultFieldPermissionJson(listForm.Name), + PivotSettingsJson = DefaultPivotSettingsJson + }, + new() { + ListFormCode = listForm.ListFormCode, + CultureName = LanguageCodes.En, + SourceDbType = DbType.String, + FieldName = "RoutePath", + EditGroupOrderNo = 1, + EditOrderNo = 2, + EditorType2 = EditorTypes.dxTextBox, + ColSpan = 1, + CaptionName = "App.Platform.RoutePath", + Width = 0, + ListOrderNo = 3, + Visible = true, + IsActive = true, + + AllowSearch = true, + // Rota "/" ile baslamalidir; sunucu tarafinda da ayni kural dogrulanir. + ValidationRuleJson = JsonSerializer.Serialize(new ValidationRuleDto[] { + new() { Type = Enum.GetName(UiColumnValidationRuleTypeEnum.required) }, + new() { Type = Enum.GetName(UiColumnValidationRuleTypeEnum.pattern), Pattern = "^/.*", Message = "Route path must start with /" }, + }), + ColumnCustomizationJson = DefaultColumnCustomizationJson, + PermissionJson = DefaultFieldPermissionJson(listForm.Name), + PivotSettingsJson = DefaultPivotSettingsJson + }, + new() { + ListFormCode = listForm.ListFormCode, + CultureName = LanguageCodes.En, + SourceDbType = DbType.String, + FieldName = "Description", + EditGroupOrderNo = 1, + EditOrderNo = 3, + EditorType2 = EditorTypes.dxTextArea, + ColSpan = 2, + EditorOptions = EditorOptions.Multiline(60), + CaptionName = "App.Listform.ListformField.Description", + Width = 0, + ListOrderNo = 4, + Visible = true, + IsActive = true, + + AllowSearch = true, + ColumnCustomizationJson = DefaultColumnCustomizationJson, + PermissionJson = DefaultFieldPermissionJson(listForm.Name), + PivotSettingsJson = DefaultPivotSettingsJson + }, + new() { + ListFormCode = listForm.ListFormCode, + CultureName = LanguageCodes.En, + SourceDbType = DbType.Boolean, + FieldName = "IsActive", + EditGroupOrderNo = 1, + EditOrderNo = 4, + EditorType2 = EditorTypes.dxCheckBox, + ColSpan = 1, + CaptionName = "App.Listform.ListformField.IsActive", + Width = 0, + ListOrderNo = 5, + Visible = true, + IsActive = true, + + AllowSearch = true, + ColumnCustomizationJson = DefaultColumnCustomizationJson, + PermissionJson = DefaultFieldPermissionJson(listForm.Name), + PivotSettingsJson = DefaultPivotSettingsJson + }, + new() { + ListFormCode = listForm.ListFormCode, + CultureName = LanguageCodes.En, + SourceDbType = DbType.String, + FieldName = "Dependencies", + EditorType2 = EditorTypes.dxTextBox, + CaptionName = "App.Platform.Dependencies", + Width = 0, + ListOrderNo = 6, + Visible = true, + IsActive = true, + + AllowSearch = true, + ColumnCustomizationJson = DefaultColumnCustomizationJson, + PermissionJson = DefaultFieldPermissionJson(listForm.Name), + PivotSettingsJson = DefaultPivotSettingsJson + }, + new() { + ListFormCode = listForm.ListFormCode, + CultureName = LanguageCodes.En, + SourceDbType = DbType.String, + FieldName = "Code", + CaptionName = "App.Listform.ListformField.Code", + Width = 0, + ListOrderNo = 7, + Visible = false, + IsActive = true, + + ColumnCustomizationJson = DefaultColumnCustomizationJson, + PermissionJson = DefaultFieldPermissionJson(listForm.Name), + PivotSettingsJson = DefaultPivotSettingsJson + }, + new() { + ListFormCode = listForm.ListFormCode, + CultureName = LanguageCodes.En, + SourceDbType = DbType.String, + FieldName = "Props", + CaptionName = "App.Listform.ListformField.Props", + Width = 0, + ListOrderNo = 8, + Visible = false, + IsActive = true, + + ColumnCustomizationJson = DefaultColumnCustomizationJson, + PermissionJson = DefaultFieldPermissionJson(listForm.Name), + PivotSettingsJson = DefaultPivotSettingsJson + }, + new() { + ListFormCode = listForm.ListFormCode, + CultureName = LanguageCodes.En, + SourceDbType = DbType.String, + FieldName = "DataSources", + CaptionName = "App.Listform.ListformField.DataSources", + Width = 0, + ListOrderNo = 9, + Visible = false, + IsActive = true, + + ColumnCustomizationJson = DefaultColumnCustomizationJson, + PermissionJson = DefaultFieldPermissionJson(listForm.Name), + PivotSettingsJson = DefaultPivotSettingsJson + }, + ]); + #endregion + } + #endregion + #region Products listFormName = AppCodes.Orders.Products; if (!await _listFormRepository.AnyAsync(a => a.ListFormCode == listFormName)) diff --git a/api/src/Sozsoft.Platform.DbMigrator/Migrations/MenusData.json b/api/src/Sozsoft.Platform.DbMigrator/Migrations/MenusData.json index 580ea277..bf017332 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Migrations/MenusData.json +++ b/api/src/Sozsoft.Platform.DbMigrator/Migrations/MenusData.json @@ -329,20 +329,12 @@ "authority": ["App.SqlQueryManager"] }, { - "key": "admin.developerkit.components", - "path": "/admin/developerkit/components", - "componentType": "normal", - "componentPath": "@/views/developerKit/ComponentManagerPage", - "routeType": "protected", - "authority": ["App.DeveloperKit.Components"] - }, - { - "key": "admin.developerkit.components.edit", - "path": "/admin/developerkit/components/edit/:id", + "key": "admin.developerkit.components.design", + "path": "/admin/developerkit/components/design/:id", "componentType": "normal", "componentPath": "@/views/developerKit/ComponentCodeLayout", "routeType": "protected", - "authority": ["App.DeveloperKit.Components"] + "authority": ["App.DeveloperKit.CustomComponents"] }, { "key": "admin.intranet.documents", @@ -997,12 +989,12 @@ }, { "ParentCode": "App.DeveloperKit", - "Code": "App.DeveloperKit.Components", - "DisplayName": "App.DeveloperKit.Components", + "Code": "App.DeveloperKit.CustomComponents", + "DisplayName": "App.DeveloperKit.CustomComponents", "Order": 5, - "Url": "/admin/developerkit/components", + "Url": "/admin/list/App.DeveloperKit.CustomComponents", "Icon": "FcBiohazard", - "RequiredPermissionName": "App.DeveloperKit.Components", + "RequiredPermissionName": "App.DeveloperKit.CustomComponents", "IsDisabled": false, "ModuleId": "Sas" }, diff --git a/api/src/Sozsoft.Platform.DbMigrator/Migrations/PermissionsData.json b/api/src/Sozsoft.Platform.DbMigrator/Migrations/PermissionsData.json index dc9d4fac..4f984eed 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Migrations/PermissionsData.json +++ b/api/src/Sozsoft.Platform.DbMigrator/Migrations/PermissionsData.json @@ -2374,7 +2374,7 @@ "GroupName": "App.Saas", "Name": "App.DeveloperKit.CustomEndpoints.Put", "ParentName": "App.DeveloperKit.CustomEndpoints", - "DisplayName": "Put", + "DisplayName": "App.StaticLookup.Put", "IsEnabled": true, "MultiTenancySide": 2, "Modules": "Sas|Adm" @@ -2471,17 +2471,17 @@ }, { "GroupName": "App.Saas", - "Name": "App.DeveloperKit.Components", + "Name": "App.DeveloperKit.CustomComponents", "ParentName": "App.DeveloperKit", - "DisplayName": "App.DeveloperKit.Components", + "DisplayName": "App.DeveloperKit.CustomComponents", "IsEnabled": true, "MultiTenancySide": 2, "Modules": "Sas|Adm" }, { "GroupName": "App.Saas", - "Name": "App.DeveloperKit.Components.Create", - "ParentName": "App.DeveloperKit.Components", + "Name": "App.DeveloperKit.CustomComponents.Create", + "ParentName": "App.DeveloperKit.CustomComponents", "DisplayName": "App.Platform.Create", "IsEnabled": true, "MultiTenancySide": 2, @@ -2489,8 +2489,8 @@ }, { "GroupName": "App.Saas", - "Name": "App.DeveloperKit.Components.Update", - "ParentName": "App.DeveloperKit.Components", + "Name": "App.DeveloperKit.CustomComponents.Update", + "ParentName": "App.DeveloperKit.CustomComponents", "DisplayName": "App.Platform.Update", "IsEnabled": true, "MultiTenancySide": 2, @@ -2498,13 +2498,31 @@ }, { "GroupName": "App.Saas", - "Name": "App.DeveloperKit.Components.Delete", - "ParentName": "App.DeveloperKit.Components", + "Name": "App.DeveloperKit.CustomComponents.Delete", + "ParentName": "App.DeveloperKit.CustomComponents", "DisplayName": "App.Platform.Delete", "IsEnabled": true, "MultiTenancySide": 2, "Modules": "Sas|Adm" }, + { + "GroupName": "App.Saas", + "Name": "App.DeveloperKit.CustomComponents.Export", + "ParentName": "App.DeveloperKit.CustomComponents", + "DisplayName": "Export", + "IsEnabled": true, + "MultiTenancySide": 2, + "Modules": "Sas|Adm" + }, + { + "GroupName": "App.Saas", + "Name": "App.DeveloperKit.CustomComponents.Import", + "ParentName": "App.DeveloperKit.CustomComponents", + "DisplayName": "Import", + "IsEnabled": true, + "MultiTenancySide": 2, + "Modules": "Sas|Adm" + }, { "GroupName": "App.Saas", "Name": "App.DeveloperKit.IntranetWidgets", diff --git a/api/src/Sozsoft.Platform.Domain.Shared/PlatformConsts.cs b/api/src/Sozsoft.Platform.Domain.Shared/PlatformConsts.cs index 6fb082e2..85483b82 100644 --- a/api/src/Sozsoft.Platform.Domain.Shared/PlatformConsts.cs +++ b/api/src/Sozsoft.Platform.Domain.Shared/PlatformConsts.cs @@ -494,7 +494,7 @@ public static class PlatformConsts /// public static class Components { - public const string Component = Default + ".Components"; + public const string Component = Default + ".CustomComponents"; public const string Create = Component + ".Create"; public const string Update = Component + ".Update"; diff --git a/api/src/Sozsoft.Platform.Domain/Data/CustomComponentDataSourceResolver.cs b/api/src/Sozsoft.Platform.Domain/Data/CustomComponentDataSourceResolver.cs index b4926f73..0c2bfc09 100644 --- a/api/src/Sozsoft.Platform.Domain/Data/CustomComponentDataSourceResolver.cs +++ b/api/src/Sozsoft.Platform.Domain/Data/CustomComponentDataSourceResolver.cs @@ -9,9 +9,8 @@ namespace Sozsoft.Platform.Data.Seeds; /// Component'in designer dokumanindaki (Props) Data sekmesi endpoint'lerini cozup /// CRUD endpoint katalogu ile eslestiren ortak mantik. /// -/// Ayni cozum hem kaydetme sirasinda (CustomComponentAppService, katalog veritabanindan gelir) -/// hem de seed sirasinda (CustomComponentDataSeeder, katalog crud/*.json dosyalarindan gelir) -/// kullanilir; boylece DataSources sutunu iki yolda da ayni sekilde uretilir. +/// Cozum kaydetme sirasinda (CustomComponentAppService) katalogu veritabanindan okur; +/// boylece DataSources sutunu tasarimci dokumanindan tutarli sekilde uretilir. /// /// public static class CustomComponentDataSourceResolver diff --git a/api/src/Sozsoft.Platform.Domain/Data/CustomComponentSeedDataSource.cs b/api/src/Sozsoft.Platform.Domain/Data/CustomComponentSeedDataSource.cs new file mode 100644 index 00000000..ba1db17c --- /dev/null +++ b/api/src/Sozsoft.Platform.Domain/Data/CustomComponentSeedDataSource.cs @@ -0,0 +1,28 @@ +namespace Sozsoft.Platform.Data.Seeds; + +/// +/// Component'in kullandigi tek bir API. dolu ise endpoint +/// CRUD Endpoint Manager tarafindan uretilmistir ve tanimi +/// (kapsam klasorune gore crud/{EntityName}.json) dosyasindadir. +/// +public class CustomComponentSeedDataSourceDto +{ + /// Designer'daki gorunur ad. + public string Name { get; set; } = string.Empty; + + public string Method { get; set; } = string.Empty; + + public string Url { get; set; } = string.Empty; + + /// Yanit icindeki liste yolu; bos ise yanitin kendisi kullanilir. + public string? ResponsePath { get; set; } + + /// Endpoint bir CRUD endpoint'ine denk geliyorsa entity adi, aksi halde null. + public string? EntityName { get; set; } + + /// CRUD endpoint'inin islem turu (GetAll, GetById, Create, Update, Delete). + public string? OperationType { get; set; } + + /// Endpoint tanimini tasiyan seed dosyasinin kapsam klasorune gore yolu. + public string? SeedFile { get; set; } +} diff --git a/api/src/Sozsoft.Platform.Domain/Data/CustomComponentSeedFile.cs b/api/src/Sozsoft.Platform.Domain/Data/CustomComponentSeedFile.cs deleted file mode 100644 index 13758b1c..00000000 --- a/api/src/Sozsoft.Platform.Domain/Data/CustomComponentSeedFile.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Sozsoft.Platform.Data.Seeds; - -/// -/// Bir custom component'in seed dosyasi. -/// Component Manager uzerinden kaydedildiginde/silindiginde -/// {host|tenants/{tenantId}}/custom/{Name}.json olarak yazilir; veritabani silinip -/// yeniden olusturuldugunda CustomComponentDataSeeder ayni dosyalari okuyarak -/// component'leri geri yukler. -/// -/// Dosya duzeni bilincli olarak TenantData.json'daki CustomComponents bloguyla aynidir; -/// boylece eski seed icerigi dogrudan tasinabilir. Liste birden fazla component tasiyabilse de -/// senkronizasyon her component icin tek dosya yazar. -/// -/// -public class CustomComponentSeedFileDto -{ - /// Dosyanin en son yazildigi an (UTC) — yalnizca bilgi amaclidir. - public DateTime GeneratedAt { get; set; } - - public List CustomComponents { get; set; } = []; -} - -/// Component'in seed edilebilir tam tanimi; dosya adi ile ayni olmasi beklenir. -public class CustomComponentSeedItemDto -{ - public string Name { get; set; } = string.Empty; - public string RoutePath { get; set; } = string.Empty; - public string Code { get; set; } = string.Empty; - public string? Props { get; set; } - public string? Description { get; set; } - public bool IsActive { get; set; } = true; - - /// Bagimli olunan component adlari; veritabaninda JSON string olarak tutulur. - public List Dependencies { get; set; } = []; - - /// - /// Component'in Data sekmesinde kullandigi API'ler; veritabaninda JSON string olarak tutulur. - /// Kaydetme sirasinda designer dokumanindan otomatik uretilir. - /// - public List DataSources { get; set; } = []; -} - -/// -/// Component'in kullandigi tek bir API. dolu ise endpoint -/// CRUD Endpoint Manager tarafindan uretilmistir ve tanimi -/// (kapsam klasorune gore crud/{EntityName}.json) dosyasindadir. -/// -public class CustomComponentSeedDataSourceDto -{ - /// Designer'daki gorunur ad. - public string Name { get; set; } = string.Empty; - - public string Method { get; set; } = string.Empty; - - public string Url { get; set; } = string.Empty; - - /// Yanit icindeki liste yolu; bos ise yanitin kendisi kullanilir. - public string? ResponsePath { get; set; } - - /// Endpoint bir CRUD endpoint'ine denk geliyorsa entity adi, aksi halde null. - public string? EntityName { get; set; } - - /// CRUD endpoint'inin islem turu (GetAll, GetById, Create, Update, Delete). - public string? OperationType { get; set; } - - /// Endpoint tanimini tasiyan seed dosyasinin kapsam klasorune gore yolu. - public string? SeedFile { get; set; } -} diff --git a/api/src/Sozsoft.Platform.Domain/Data/ListFormSeedDataApplier.cs b/api/src/Sozsoft.Platform.Domain/Data/ListFormSeedDataApplier.cs index 4689215a..dc94f87d 100644 --- a/api/src/Sozsoft.Platform.Domain/Data/ListFormSeedDataApplier.cs +++ b/api/src/Sozsoft.Platform.Domain/Data/ListFormSeedDataApplier.cs @@ -28,6 +28,40 @@ public interface IListFormSeedDataApplier { /// Verilen kapsamdaki tum veri seed dosyalarini uygular. Task ApplyAsync(Guid? tenantId); + + /// + /// Kapsam klasorune goreli belirli veri seed dosyalarini uygular; import gibi dosya bazli + /// akislar butun klasoru taramak yerine yalnizca yazilan dosyalari uygular. Dosyalar + /// ApplyAsync ile ayni kurala gore siralanir. + /// + /// + /// true ise anahtari veritabaninda bulunan satir dosyadaki degerlerle guncellenir. Migrate/seed + /// akisi bunu kullanmaz — orada dosya eski olabilir ve canli kayit ezilmemelidir; import ise + /// kullanicinin bilincli olarak getirdigi icerik oldugu icin gunceller. + /// + Task> ApplyFilesAsync( + Guid? tenantId, + IReadOnlyCollection relativePaths, + bool overwriteExisting = false); +} + +/// +/// Tek bir veri seed dosyasinin uygulama sonucu. dolu ise dosya islenemedi; +/// diger dosyalarin uygulanmasi bundan etkilenmez. +/// +public record ListFormSeedDataApplyResult(string Path, int Inserted, int Updated, int Skipped, string? Error = null); + +/// Bir satirin veritabaninda ne olduguna dair sonuc. +public enum SeedRowOutcome +{ + /// Kayit yoktu, eklendi. + Inserted, + + /// Kayit vardi, dosyadaki degerlerle guncellendi. + Updated, + + /// Kayit vardi, dokunulmadi. + Skipped, } /// @@ -60,6 +94,50 @@ public class ListFormSeedDataApplier( /// Cozulen kullanici anahtarlari; ayni kullanici her satirda yeniden sorgulanmaz. private readonly Dictionary userIdCache = []; + public async Task> ApplyFilesAsync( + Guid? tenantId, + IReadOnlyCollection relativePaths, + bool overwriteExisting = false) + { + if (relativePaths == null || relativePaths.Count == 0) + return []; + + var scopeRoot = Path.GetFullPath(SeedPathResolver.GetScopePath(configuration, tenantId)); + var results = new List(); + var filePaths = new List(); + + foreach (var relativePath in relativePaths) + { + var normalized = relativePath.Replace('\\', '/').Trim('/'); + var fullPath = normalized.Contains("..", StringComparison.Ordinal) + ? null + : Path.GetFullPath(Path.Combine(scopeRoot, normalized.Replace('/', Path.DirectorySeparatorChar))); + + if (fullPath == null + || !fullPath.StartsWith(scopeRoot + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase) + || !File.Exists(fullPath)) + { + results.Add(new ListFormSeedDataApplyResult(normalized, 0, 0, 0, "Seed data file not found.")); + continue; + } + + filePaths.Add(fullPath); + } + + using (currentTenant.Change(tenantId)) + { + foreach (var filePath in OrderFiles([.. filePaths])) + { + // Her dosya kendi unit of work'unde islenir; ApplyAsync ile ayni kural. + using var uow = unitOfWorkManager.Begin(requiresNew: true, isTransactional: false); + results.Add(await ApplyFileAsync(filePath, overwriteExisting)); + await uow.CompleteAsync(); + } + } + + return results; + } + public async Task ApplyAsync(Guid? tenantId) { var scopeFolderName = SeedPathResolver.GetScopeFolderName(tenantId); @@ -87,7 +165,7 @@ public class ListFormSeedDataApplier( // Seed contributor'larin unit of work'u kapandigi icin her dosya kendi // unit of work'unde islenir; aksi halde DbContext disposed gelir. using var uow = unitOfWorkManager.Begin(requiresNew: true, isTransactional: false); - await ApplyFileAsync(filePath); + await ApplyFileAsync(filePath, overwriteExisting: false); await uow.CompleteAsync(); } } @@ -126,7 +204,11 @@ public class ListFormSeedDataApplier( } } - private async Task ApplyFileAsync(string filePath) + /// + /// Dosyayi okur ve satirlari ilgili ListForm'un tablosuna uygular. Tek bir dosyanin hatasi + /// diger dosyalari etkilemez; hata sonuca tasinir ve loglanir. + /// + private async Task ApplyFileAsync(string filePath, bool overwriteExisting) { var fileName = Path.GetFileName(filePath); try @@ -142,14 +224,14 @@ public class ListFormSeedDataApplier( if (seedFile?.Rows == null || seedFile.Rows.Count == 0) { logger.LogWarning("[{File}] No row found, skipped.", fileName); - return; + return new ListFormSeedDataApplyResult(fileName, 0, 0, 0); } var listForm = await listFormRepository.FirstOrDefaultAsync(a => a.ListFormCode == listFormCode); if (listForm == null) { logger.LogWarning("[{File}] ListForm '{ListFormCode}' not found, skipped.", fileName, listFormCode); - return; + return new ListFormSeedDataApplyResult(fileName, 0, 0, 0, $"ListForm '{listFormCode}' not found."); } var fields = await listFormFieldRepository.GetListAsync(a => a.ListFormCode == listFormCode); @@ -160,38 +242,53 @@ public class ListFormSeedDataApplier( var (repository, connectionString, dataSourceType) = await dynamicDataManager.GetAsync(listForm.IsTenant, listForm.DataSourceCode); var inserted = 0; + var updated = 0; var skipped = 0; foreach (var row in seedFile.Rows) { - if (await ApplyRowAsync(listForm, fields, keyFieldName, row, repository, connectionString, dataSourceType)) - inserted++; - else - skipped++; + switch (await ApplyRowAsync(listForm, fields, keyFieldName, row, repository, connectionString, dataSourceType, overwriteExisting)) + { + case SeedRowOutcome.Inserted: + inserted++; + break; + case SeedRowOutcome.Updated: + updated++; + break; + default: + skipped++; + break; + } } - logger.LogInformation("[{File}] {Inserted} row(s) inserted, {Skipped} existing row(s) skipped.", fileName, inserted, skipped); + logger.LogInformation( + "[{File}] {Inserted} row(s) inserted, {Updated} row(s) updated, {Skipped} existing row(s) skipped.", + fileName, inserted, updated, skipped); + return new ListFormSeedDataApplyResult(fileName, inserted, updated, skipped); } catch (Exception ex) { // Tek bir dosyanin hatasi diger listelerin seed edilmesini engellemez. logger.LogError(ex, "Failed to process list form data seed file: {FileName}", fileName); + return new ListFormSeedDataApplyResult(fileName, 0, 0, 0, ex.Message); } } /// - /// Satiri ekler; kayit eklendiyse true, ayni kayit zaten varsa false doner. + /// Satiri veritabanina yazar. Kayit yoksa eklenir; varsa + /// true ise dosyadaki degerlerle guncellenir ve soft delete izleri temizlenir, degilse dokunulmaz. /// Dosyada anahtar yoksa (identity kolonlu tablolar) satir diger alanlarinin tamami uzerinden - /// eslesir ve anahtar veritabanina biraktirilir. + /// eslesir, anahtar veritabanina biraktirilir ve guncelleme yapilamaz. /// - private async Task ApplyRowAsync( + private async Task ApplyRowAsync( ListForm listForm, List fields, string keyFieldName, Dictionary row, IDynamicDataRepository repository, string connectionString, - DataSourceTypeEnum dataSourceType) + DataSourceTypeEnum dataSourceType, + bool overwriteExisting) { var keyField = fields.FirstOrDefault(a => a.FieldName == keyFieldName); var keyValue = QueryHelper.GetFormattedValue( @@ -201,9 +298,9 @@ public class ListFormSeedDataApplier( object[] keys = [keyValue]; var hasKey = keyValue != null; - var insertParameters = BuildParameters(fields, await ResolveTokensAsync(row), keyFieldName, includeKey: hasKey); + var resolvedRow = await ResolveTokensAsync(row); + var insertParameters = BuildParameters(fields, resolvedRow, keyFieldName, includeKey: hasKey); - // Var olan kayda dokunulmaz: veritabanindaki guncel deger, dosyadaki eski degerle ezilmemeli. var match = hasKey ? new Dictionary { [keyFieldName] = keyValue } : new Dictionary(insertParameters); @@ -215,7 +312,31 @@ public class ListFormSeedDataApplier( if (await ExistsAsync(listForm, match, repository, connectionString)) { - return false; + // Anahtarsiz satirin hangi kayda karsilik geldigi bilinmedigi icin guncellenemez. + if (!overwriteExisting || !hasKey) + return SeedRowOutcome.Skipped; + + // Anahtar SET listesine girmez; GenerateQuery onu WHERE icin parametrelere ekler. + var updateParameters = BuildParameters(fields, resolvedRow, keyFieldName, includeKey: false); + + // Kayit daha once silinmis olabilir: satir tabloda durur ama ekranin varsayilan + // suzgeci ("IsDeleted" = 'false') onu gizler. Dosyadan gelen icerik kaydin var olmasi + // gerektigini soyledigi icin silinme izleri temizlenir, aksi halde import sessizce + // gorunmeyen bir kayit gunceller. + foreach (var reset in BuildSoftDeleteResetParameters(listForm)) + { + if (!updateParameters.ContainsKey(reset.Key)) + updateParameters[reset.Key] = reset.Value; + } + + if (updateParameters.Count == 0) + return SeedRowOutcome.Skipped; + + await MergeDefaultsAsync(listForm, fields, OperationEnum.Update, keys, updateParameters); + + var updateSql = queryManager.GenerateQuery(listForm, fields, updateParameters, OperationEnum.Update, dataSourceType, keys); + await repository.ExecuteAsync(updateSql, connectionString, updateParameters); + return SeedRowOutcome.Updated; } await MergeDefaultsAsync(listForm, fields, OperationEnum.Insert, keys, insertParameters); @@ -235,7 +356,32 @@ public class ListFormSeedDataApplier( var insertSql = queryManager.GenerateQuery(listForm, fields, insertParameters, OperationEnum.Insert, dataSourceType, keys); await repository.ExecuteAsync(insertSql, connectionString, insertParameters); - return true; + return SeedRowOutcome.Inserted; + } + + /// + /// Ekranin silme komutunun yazdigi soft delete kolonlarini bulup onlari sifirlayan + /// parametreleri dondurur. Kolon listesi komuttan okunur; boylece yalnizca gercekten var olan + /// kolonlara yazilir ve donanimsal (hard) silme yapan bir ekranda hicbir sey eklenmez. + /// + private static Dictionary BuildSoftDeleteResetParameters(ListForm listForm) + { + var command = listForm.DeleteCommand; + var parameters = new Dictionary(); + + if (string.IsNullOrWhiteSpace(command)) + return parameters; + + if (command.Contains("IsDeleted", StringComparison.OrdinalIgnoreCase)) + parameters["IsDeleted"] = false; + + if (command.Contains("DeletionTime", StringComparison.OrdinalIgnoreCase)) + parameters["DeletionTime"] = null; + + if (command.Contains("DeleterId", StringComparison.OrdinalIgnoreCase)) + parameters["DeleterId"] = null; + + return parameters; } /// Verilen alan degerleriyle eslesen bir kayit hedef tabloda var mi. diff --git a/api/src/Sozsoft.Platform.Domain/Data/SeedConsts.cs b/api/src/Sozsoft.Platform.Domain/Data/SeedConsts.cs index f06a5c1e..75f9cd5a 100644 --- a/api/src/Sozsoft.Platform.Domain/Data/SeedConsts.cs +++ b/api/src/Sozsoft.Platform.Domain/Data/SeedConsts.cs @@ -476,6 +476,7 @@ public static class SeedConsts public const string IntranetWidgets = Default + ".IntranetWidgets"; public const string DynamicServices = Default + ".DynamicServices"; + public const string CustomComponents = Default + ".CustomComponents"; } public const string Forum = Prefix.App + ".Forum"; diff --git a/api/src/Sozsoft.Platform.Domain/Data/SeedPathResolver.cs b/api/src/Sozsoft.Platform.Domain/Data/SeedPathResolver.cs index e6c02069..a0c0ca47 100644 --- a/api/src/Sozsoft.Platform.Domain/Data/SeedPathResolver.cs +++ b/api/src/Sozsoft.Platform.Domain/Data/SeedPathResolver.cs @@ -23,7 +23,6 @@ namespace Sozsoft.Platform.Data.Seeds; /// seeds/ /// host/ → host (varsayilan) veritabani kapsami /// crud/ -/// custom/ /// wizard/ /// data/ /// sql/ → SQL Server saglayicisi @@ -34,7 +33,7 @@ namespace Sozsoft.Platform.Data.Seeds; /// execute/ /// tenants/ /// {tenantId}/ → her tenant kendi klasorunde -/// crud/ custom/ wizard/ data/ sql/{object,execute}/ postgres/{object,execute}/ +/// crud/ wizard/ data/ sql/{object,execute}/ postgres/{object,execute}/ /// /// /// @@ -72,9 +71,6 @@ public static class SeedPathResolver /// Crud endpoint konfigurasyon dosyalari. public const string CrudFolder = "crud"; - /// Custom component konfigurasyon dosyalari. - public const string CustomFolder = "custom"; - /// Liste verisinin aynalandigi seed dosyalari (data/{ListFormCode}.json). public const string DataFolder = "data"; diff --git a/claude.md b/claude.md index 97f4b5f5..5bf20dfe 100644 --- a/claude.md +++ b/claude.md @@ -184,15 +184,21 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` → `permission.constant.ts` + `LanguagesData.json`. - **Yeni bir dil anahtarı** → `LanguagesData.json`; eklemeden önce anahtarın zaten var olup olmadığını kontrol et. -- **Custom component kaydı/silinmesi** → `custom/{Name}.json`; kullandığı endpoint'ler - `crud/{Entity}.json`. -- **Wizard dosyası** → tek başına taşınmaz; export zip'i `wizard/`, `custom/`, `crud/` ve - `{sql|postgres}/{object|execute}/` bağımlılıklarını da içerir. +- **Custom component kaydı/silinmesi** → `data/App.DeveloperKit.CustomComponents.json` + (Custom Components ekranının seed verisi); bileşenin kendi dosyası yoktur, her bileşen bu + dosyada bir satırdır. Hem listeden hem Visual Designer'dan (`CustomComponentAppService`) yapılan + kayıt aynı dosyaya yansır; kullandığı endpoint'ler `crud/{Entity}.json`. +- **Wizard dosyası** → tek başına taşınmaz; export zip'i `wizard/`, `data/`, `crud/` ve + `{sql|postgres}/{object|execute}/` bağımlılıklarını da içerir. `Custom` yolunda `data/` girdisi + yalnızca ilgili bileşen ve bağımlılık zincirine süzülür; import'ta hedef dosyanın üstüne + yazılmaz, satırlar anahtar üzerinden birleştirilir. - **`ListForm.SeedFilePath` dolu bir ekran** → `SeedSyncInsert/SeedSyncUpdate/SeedSyncDelete` bayraklarından işaretli olan işlemler kapsam klasöründeki `data/{ListFormCode}.json` dosyasına - yansır (`ListFormSeedDataSynchronizer`); migrate/seed sırasında `ListFormSeedDataApplier` dosyayı - geri uygular (kayıt yoksa ekler, varsa dokunmaz). Yeni bir kayıt yolu (yeni endpoint, toplu işlem) - eklenirse senkronizasyon çağrısı da eklenir; yoksa dosya bayatlar. + yansır (`ListFormSeedDataSynchronizer`); migrate/seed sırasında ve wizard import'unun kapanışında + (`CompleteImport`) `ListFormSeedDataApplier` dosyayı geri uygular. Migrate/seed'de kayıt yoksa + eklenir, varsa dokunulmaz; import'ta `overwriteExisting` açıktır: var olan kayıt dosyadaki + değerlerle güncellenir ve soft delete izleri (`IsDeleted`/`DeletionTime`/`DeleterId`) temizlenir. Yeni bir kayıt yolu (yeni endpoint, toplu işlem, tipli AppService) eklenirse + senkronizasyon çağrısı da eklenir; yoksa dosya bayatlar. - **`MenusData.json`** → `Routes` bölümüne dokunma; yalnızca `Modules` ve `Menus`. - **Çok değerli alan (`dxTagBox` / çoklu `dxGridBox`)** → tek metin kolonunda `|` ile saklanır. Ekran varsayılan `list-form-data/*` ucundan kaydediyorsa birleştirmeyi `QueryHelper` yapar; diff --git a/configs/seeds/host/custom/DynamicEntityComponent.json b/configs/seeds/host/custom/DynamicEntityComponent.json deleted file mode 100644 index 6dc2ec5f..00000000 --- a/configs/seeds/host/custom/DynamicEntityComponent.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "GeneratedAt": "2026-08-20T11:16:38.2313189Z", - "CustomComponents": [ - { - "Name": "DynamicEntityComponent", - "RoutePath": "/admin/dynamic-entity", - "Code": "import React, { useEffect, useState } from \"react\";\nimport axios from \"axios\";\n\ninterface DynamicEntityComponentProps {\n title: string;\n}\n\nconst api = axios.create({\n baseURL: \"https://localhost:44344/\",\n});\n\nconst DynamicEntityComponent: React.FC = ({ title }) => {\n const [data, setData] = useState>([]);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState(null);\n\n useEffect(() => {\n const fetchData = async () => {\n setLoading(true);\n setError(null);\n\n try {\n const res = await api.get(`/api/app/crudendpoint/${title}`);\n const raw = Array.isArray(res.data) ? res.data : res.data?.items ?? [];\n\n const filtered = raw.map((item: any) => ({\n id: item.Id ?? item.id,\n name: item.Name ?? item.name,\n }));\n\n setData(filtered);\n } catch (err: any) {\n setError(err.message || \"Failed to fetch data\");\n } finally {\n setLoading(false);\n }\n };\n\n if (title) fetchData();\n }, [title]);\n\n if (loading) return
Loading...
;\n if (error) return
Error: {error}
;\n if (!data.length) return
No records found
;\n\n const headers = [\"id\", \"name\", \"actions\"];\n\n return (\n
\n \n \n \n {headers.map((key) => (\n \n {key === \"actions\" ? \"Actions\" : key}\n \n ))}\n \n \n \n {data.map((item, rowIndex) => (\n \n \n \n \n \n ))}\n \n
\n {item.id}\n \n {item.name}\n \n alert(item.name)}\n shape=\"round\"\n size=\"xs\"\n variant=\"solid\"\n id=\"c_mrix4c1a_qrybhk\"\n >\n Show Name\n \n
\n
\n );\n};\n\nexport default DynamicEntityComponent;", - "Props": null, - "Description": null, - "IsActive": true, - "Dependencies": [], - "DataSources": [] - } - ] -} \ No newline at end of file diff --git a/configs/seeds/host/custom/NewComponent.json b/configs/seeds/host/custom/NewComponent.json deleted file mode 100644 index 0c4bbf1b..00000000 --- a/configs/seeds/host/custom/NewComponent.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "GeneratedAt": "2026-08-20T11:23:45.7859641Z", - "CustomComponents": [ - { - "Name": "NewComponent", - "RoutePath": "/admin/new-component", - "Code": "/*__SOZSOFT_VISUAL_DESIGNER__%7B%22version%22%3A1%2C%22sourceMode%22%3A%22visual%22%2C%22nodes%22%3A%5B%5D%2C%22canvas%22%3A%7B%22width%22%3A%22responsive%22%7D%2C%22lifecycle%22%3A%7B%22onMount%22%3A%22%22%7D%2C%22dataSources%22%3A%5B%5D%7D__*/\nconst NewComponent = () => {\n\n\n return (\n <>\n\n \n )\n}\n\nexport default NewComponent", - "Props": "{\"visualDesigner\":{\"version\":1,\"sourceMode\":\"visual\",\"nodes\":[],\"canvas\":{\"width\":\"responsive\"},\"lifecycle\":{\"onMount\":\"\"},\"dataSources\":[]}}", - "Description": "", - "IsActive": true, - "Dependencies": [], - "DataSources": [] - } - ] -} \ No newline at end of file diff --git a/configs/seeds/host/custom/RoleListComponent.json b/configs/seeds/host/custom/RoleListComponent.json deleted file mode 100644 index 190f0f43..00000000 --- a/configs/seeds/host/custom/RoleListComponent.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "GeneratedAt": "2026-08-18T00:00:00Z", - "CustomComponents": [ - { - "Name": "RoleListComponent", - "RoutePath": "/admin/roles-list", - "Code": "const RoleListComponent = ({\n title = \"AbpRoles\"\n}) => {\n return (\n \n );\n};\n\nexport default RoleListComponent;", - "Props": null, - "Description": null, - "IsActive": true, - "Dependencies": [ - "DynamicEntityComponent" - ], - "DataSources": [] - } - ] -} \ No newline at end of file diff --git a/configs/seeds/host/custom/RoleComponent.json b/configs/seeds/host/data/App.DeveloperKit.CustomComponents.json similarity index 92% rename from configs/seeds/host/custom/RoleComponent.json rename to configs/seeds/host/data/App.DeveloperKit.CustomComponents.json index 6f662c07..85e3eaba 100644 --- a/configs/seeds/host/custom/RoleComponent.json +++ b/configs/seeds/host/data/App.DeveloperKit.CustomComponents.json @@ -1,14 +1,55 @@ { - "GeneratedAt": "2026-09-03T07:41:13.618317Z", - "CustomComponents": [ + "ListFormCode": "App.DeveloperKit.CustomComponents", + "KeyFieldName": "Id", + "GeneratedAt": "2026-09-05T18:02:07.557592Z", + "Order": 0, + "Rows": [ { - "Name": "RoleComponent", - "RoutePath": "/admin/roles", - "Code": "/*__SOZSOFT_VISUAL_DESIGNER__%7B%22version%22%3A1%2C%22sourceMode%22%3A%22visual%22%2C%22nodes%22%3A%5B%7B%22id%22%3A%22cmp_msyfmegt_jkkn3%22%2C%22type%22%3A%22Form%22%2C%22kind%22%3A%22layout%22%2C%22props%22%3A%7B%22selectEndpoint%22%3A%22source_5w_5ibpv%22%2C%22insertEndpoint%22%3A%22source_h3_484qa%22%2C%22updateEndpoint%22%3A%22source_gb_d9cvs%22%2C%22deleteEndpoint%22%3A%22source_0t_avok9%22%2C%22keyFieldName%22%3A%22id%22%2C%22collectionPath%22%3A%22%22%2C%22keySource%22%3A%22query%22%2C%22keyParamName%22%3A%22id%22%2C%22previewKeyValue%22%3A%22%22%2C%22autoLoad%22%3Atrue%2C%22showToolbar%22%3Atrue%2C%22gap%22%3A16%2C%22className%22%3A%22%22%2C%22title%22%3A%22%3A%3AAbpIdentity.Roles%22%2C%22columnCount%22%3A2%2C%22selectPermission%22%3A%22%22%2C%22insertPermission%22%3A%22%22%7D%2C%22events%22%3A%7B%22onLoad%22%3A%22%22%2C%22onRecordChange%22%3A%22%22%2C%22onFieldChange%22%3A%22%22%2C%22onNewRecord%22%3A%22%22%2C%22onModeChange%22%3A%22%22%2C%22onBeforeSave%22%3A%22%22%2C%22onAfterSave%22%3A%22%22%2C%22onBeforeDelete%22%3A%22%22%2C%22onAfterDelete%22%3A%22%22%2C%22onError%22%3A%22%22%7D%2C%22bindings%22%3A%7B%7D%2C%22children%22%3A%5B%7B%22id%22%3A%22cmp_msyfpfia_ifhdq%22%2C%22type%22%3A%22Input%22%2C%22kind%22%3A%22ui%22%2C%22props%22%3A%7B%22asElement%22%3A%22%22%2C%22disabled%22%3Afalse%2C%22invalid%22%3Afalse%2C%22rows%22%3A3%2C%22prefix%22%3A%22%3A%3AApp.Platform.Name%22%2C%22size%22%3A%22md%22%2C%22suffix%22%3A%22%22%2C%22textArea%22%3Afalse%2C%22type%22%3A%22text%22%2C%22unstyle%22%3Afalse%2C%22field%22%3A%22%22%2C%22form%22%3A%22%22%2C%22className%22%3A%22%22%2C%22children%22%3A%22%22%2C%22style%22%3A%7B%7D%2C%22placeholder%22%3A%22Metin%20girin...%22%2C%22value%22%3A%22%22%2C%22name%22%3A%22%22%2C%22autoComplete%22%3A%22off%22%2C%22autoFocus%22%3Afalse%2C%22readOnly%22%3Afalse%2C%22required%22%3Afalse%2C%22maxLength%22%3A%22%22%2C%22minLength%22%3A%22%22%2C%22pattern%22%3A%22%22%2C%22sqlDefaultValue%22%3A%22%22%7D%2C%22events%22%3A%7B%22onChange%22%3A%22%22%7D%2C%22bindings%22%3A%7B%22value%22%3A%7B%22sourceId%22%3A%22cmp_msyfmegt_jkkn3%22%2C%22path%22%3A%22Name%22%7D%7D%2C%22children%22%3A%5B%5D%2C%22ref%22%3A%22input1%22%7D%2C%7B%22id%22%3A%22cmp_msyjl707_crcn7%22%2C%22type%22%3A%22Input%22%2C%22kind%22%3A%22ui%22%2C%22props%22%3A%7B%22asElement%22%3A%22%22%2C%22disabled%22%3Afalse%2C%22invalid%22%3Afalse%2C%22rows%22%3A3%2C%22prefix%22%3A%22Normalized%20Name%20%3A%22%2C%22size%22%3A%22md%22%2C%22suffix%22%3A%22%22%2C%22textArea%22%3Afalse%2C%22type%22%3A%22text%22%2C%22unstyle%22%3Afalse%2C%22field%22%3A%22%22%2C%22form%22%3A%22%22%2C%22className%22%3A%22%22%2C%22children%22%3A%22%22%2C%22style%22%3A%7B%7D%2C%22placeholder%22%3A%22Metin%20girin...%22%2C%22value%22%3A%22%22%2C%22name%22%3A%22%22%2C%22autoComplete%22%3A%22off%22%2C%22autoFocus%22%3Afalse%2C%22readOnly%22%3Afalse%2C%22required%22%3Afalse%2C%22maxLength%22%3A%22%22%2C%22minLength%22%3A%22%22%2C%22pattern%22%3A%22%22%7D%2C%22events%22%3A%7B%22onChange%22%3A%22%22%7D%2C%22bindings%22%3A%7B%22value%22%3A%7B%22sourceId%22%3A%22cmp_msyfmegt_jkkn3%22%2C%22path%22%3A%22NormalizedName%22%7D%7D%2C%22children%22%3A%5B%5D%2C%22ref%22%3A%22input2%22%7D%2C%7B%22id%22%3A%22cmp_msyjdodn_u3o74%22%2C%22type%22%3A%22Input%22%2C%22kind%22%3A%22ui%22%2C%22props%22%3A%7B%22asElement%22%3A%22%22%2C%22disabled%22%3Afalse%2C%22invalid%22%3Afalse%2C%22rows%22%3A3%2C%22prefix%22%3A%22Concurrent%20Stamp%20%3A%20%22%2C%22size%22%3A%22md%22%2C%22suffix%22%3A%22%22%2C%22textArea%22%3Afalse%2C%22type%22%3A%22text%22%2C%22unstyle%22%3Afalse%2C%22field%22%3A%22%22%2C%22form%22%3A%22%22%2C%22className%22%3A%22%22%2C%22children%22%3A%22%22%2C%22style%22%3A%7B%7D%2C%22placeholder%22%3A%22Metin%20girin...%22%2C%22value%22%3A%22%22%2C%22name%22%3A%22%22%2C%22autoComplete%22%3A%22off%22%2C%22autoFocus%22%3Afalse%2C%22readOnly%22%3Afalse%2C%22required%22%3Afalse%2C%22maxLength%22%3A%22%22%2C%22minLength%22%3A%22%22%2C%22pattern%22%3A%22%22%7D%2C%22events%22%3A%7B%22onChange%22%3A%22%22%7D%2C%22bindings%22%3A%7B%22value%22%3A%7B%22sourceId%22%3A%22cmp_msyfmegt_jkkn3%22%2C%22path%22%3A%22ConcurrencyStamp%22%7D%7D%2C%22children%22%3A%5B%5D%2C%22ref%22%3A%22input3%22%7D%2C%7B%22id%22%3A%22cmp_msyje9ys_2c08a%22%2C%22type%22%3A%22Checkbox%22%2C%22kind%22%3A%22ui%22%2C%22props%22%3A%7B%22checked%22%3Afalse%2C%22color%22%3A%22blue-500%22%2C%22disabled%22%3Afalse%2C%22labelRef%22%3A%22%22%2C%22name%22%3A%22%22%2C%22readOnly%22%3Afalse%2C%22value%22%3A%22%22%2C%22field%22%3Anull%2C%22className%22%3A%22%22%2C%22children%22%3A%22Is%20Default%22%2C%22style%22%3A%7B%7D%2C%22sqlDefaultValue%22%3A%22true%22%7D%2C%22events%22%3A%7B%22onChange%22%3A%22%22%7D%2C%22bindings%22%3A%7B%22checked%22%3A%7B%22sourceId%22%3A%22cmp_msyfmegt_jkkn3%22%2C%22path%22%3A%22IsDefault%22%7D%7D%2C%22children%22%3A%5B%5D%2C%22ref%22%3A%22checkbox1%22%7D%2C%7B%22id%22%3A%22cmp_msyjebu2_27tqi%22%2C%22type%22%3A%22Checkbox%22%2C%22kind%22%3A%22ui%22%2C%22props%22%3A%7B%22checked%22%3Afalse%2C%22color%22%3A%22blue-500%22%2C%22disabled%22%3Afalse%2C%22labelRef%22%3A%22%22%2C%22name%22%3A%22%22%2C%22readOnly%22%3Afalse%2C%22value%22%3A%22%22%2C%22field%22%3Anull%2C%22className%22%3A%22%22%2C%22children%22%3A%22Is%20Static%22%2C%22style%22%3A%7B%7D%2C%22sqlDefaultValue%22%3A%22true%22%7D%2C%22events%22%3A%7B%22onChange%22%3A%22%22%7D%2C%22bindings%22%3A%7B%22checked%22%3A%7B%22sourceId%22%3A%22cmp_msyfmegt_jkkn3%22%2C%22path%22%3A%22IsStatic%22%7D%7D%2C%22children%22%3A%5B%5D%2C%22ref%22%3A%22checkbox2%22%7D%2C%7B%22id%22%3A%22cmp_msyjmetn_gojol%22%2C%22type%22%3A%22Checkbox%22%2C%22kind%22%3A%22ui%22%2C%22props%22%3A%7B%22checked%22%3Afalse%2C%22color%22%3A%22blue-500%22%2C%22disabled%22%3Afalse%2C%22labelRef%22%3A%22%22%2C%22name%22%3A%22%22%2C%22readOnly%22%3Afalse%2C%22value%22%3A%22%22%2C%22field%22%3Anull%2C%22className%22%3A%22%22%2C%22children%22%3A%22Is%20Public%22%2C%22style%22%3A%7B%7D%2C%22sqlDefaultValue%22%3A%22false%22%7D%2C%22events%22%3A%7B%22onChange%22%3A%22%22%7D%2C%22bindings%22%3A%7B%22checked%22%3A%7B%22sourceId%22%3A%22cmp_msyfmegt_jkkn3%22%2C%22path%22%3A%22IsPublic%22%7D%7D%2C%22children%22%3A%5B%5D%2C%22ref%22%3A%22checkbox3%22%7D%2C%7B%22id%22%3A%22cmp_msyjo0g8_oathk%22%2C%22type%22%3A%22Input%22%2C%22kind%22%3A%22ui%22%2C%22props%22%3A%7B%22asElement%22%3A%22%22%2C%22disabled%22%3Afalse%2C%22invalid%22%3Afalse%2C%22rows%22%3A3%2C%22prefix%22%3A%22Entity%20Version%20%3A%20%22%2C%22size%22%3A%22md%22%2C%22suffix%22%3A%22%22%2C%22textArea%22%3Afalse%2C%22type%22%3A%22number%22%2C%22unstyle%22%3Afalse%2C%22field%22%3A%22%22%2C%22form%22%3A%22%22%2C%22className%22%3A%22%22%2C%22children%22%3A%22%22%2C%22style%22%3A%7B%7D%2C%22placeholder%22%3A%22Metin%20girin...%22%2C%22value%22%3A%22%22%2C%22name%22%3A%22%22%2C%22autoComplete%22%3A%22off%22%2C%22autoFocus%22%3Afalse%2C%22readOnly%22%3Afalse%2C%22required%22%3Afalse%2C%22maxLength%22%3A%22%22%2C%22minLength%22%3A%22%22%2C%22pattern%22%3A%22%22%2C%22sqlDefaultValue%22%3A%223%22%7D%2C%22events%22%3A%7B%22onChange%22%3A%22%22%7D%2C%22bindings%22%3A%7B%22value%22%3A%7B%22sourceId%22%3A%22cmp_msyfmegt_jkkn3%22%2C%22path%22%3A%22EntityVersion%22%7D%7D%2C%22children%22%3A%5B%5D%2C%22ref%22%3A%22input4%22%7D%2C%7B%22id%22%3A%22cmp_msyjplfq_py8lz%22%2C%22type%22%3A%22Input%22%2C%22kind%22%3A%22ui%22%2C%22props%22%3A%7B%22asElement%22%3A%22%22%2C%22disabled%22%3Afalse%2C%22invalid%22%3Afalse%2C%22rows%22%3A3%2C%22prefix%22%3A%22Extra%20Properties%20%3A%22%2C%22size%22%3A%22md%22%2C%22suffix%22%3A%22%22%2C%22textArea%22%3Afalse%2C%22type%22%3A%22text%22%2C%22unstyle%22%3Afalse%2C%22field%22%3A%22%22%2C%22form%22%3A%22%22%2C%22className%22%3A%22%22%2C%22children%22%3A%22%22%2C%22style%22%3A%7B%7D%2C%22placeholder%22%3A%22Metin%20girin...%22%2C%22value%22%3A%22%22%2C%22name%22%3A%22%22%2C%22autoComplete%22%3A%22off%22%2C%22autoFocus%22%3Afalse%2C%22readOnly%22%3Afalse%2C%22required%22%3Afalse%2C%22maxLength%22%3A%22%22%2C%22minLength%22%3A%22%22%2C%22pattern%22%3A%22%22%2C%22sqlDefaultValue%22%3A%22%7B%7D%22%7D%2C%22events%22%3A%7B%22onChange%22%3A%22%22%7D%2C%22bindings%22%3A%7B%22value%22%3A%7B%22sourceId%22%3A%22cmp_msyfmegt_jkkn3%22%2C%22path%22%3A%22ExtraProperties%22%7D%7D%2C%22children%22%3A%5B%5D%2C%22ref%22%3A%22input5%22%7D%5D%2C%22ref%22%3A%22form1%22%7D%2C%7B%22id%22%3A%22cmp_mt023r5u_rb2jv%22%2C%22type%22%3A%22Grid%22%2C%22kind%22%3A%22ui%22%2C%22props%22%3A%7B%22hoverable%22%3Atrue%2C%22overflow%22%3Atrue%2C%22borderlessRow%22%3Afalse%2C%22compact%22%3Afalse%2C%22cols%22%3A%223%22%2C%22rows%22%3A%222%22%2C%22gap%22%3A%224%22%2C%22colGap%22%3A%224%22%2C%22rowGap%22%3A%224%22%2C%22responsive%22%3Atrue%2C%22smCols%22%3A%221%22%2C%22mdCols%22%3A%222%22%2C%22lgCols%22%3A%223%22%2C%22xlCols%22%3A%224%22%2C%22autoFit%22%3Afalse%2C%22minColWidth%22%3A%22200px%22%2C%22height%22%3A%22auto%22%2C%22className%22%3A%22mt-4%22%2C%22items%22%3A%5B%7B%22id%22%3A%22grid-item-1%22%2C%22content%22%3A%22Grid%20Item%201%22%2C%22colSpan%22%3A1%2C%22rowSpan%22%3A1%2C%22colStart%22%3A1%2C%22rowStart%22%3A1%2C%22className%22%3A%22bg-blue-100%20p-4%20border%20rounded%22%7D%2C%7B%22id%22%3A%22grid-item-2%22%2C%22content%22%3A%22Grid%20Item%202%22%2C%22colSpan%22%3A1%2C%22rowSpan%22%3A1%2C%22colStart%22%3A2%2C%22rowStart%22%3A1%2C%22className%22%3A%22bg-green-100%20p-4%20border%20rounded%22%7D%2C%7B%22id%22%3A%22grid-item-3%22%2C%22content%22%3A%22Grid%20Item%203%22%2C%22colSpan%22%3A1%2C%22rowSpan%22%3A1%2C%22colStart%22%3A3%2C%22rowStart%22%3A1%2C%22className%22%3A%22bg-yellow-100%20p-4%20border%20rounded%22%7D%2C%7B%22id%22%3A%22grid-item-4%22%2C%22content%22%3A%22Grid%20Item%204%22%2C%22colSpan%22%3A2%2C%22rowSpan%22%3A1%2C%22colStart%22%3A1%2C%22rowStart%22%3A2%2C%22className%22%3A%22bg-purple-100%20p-4%20border%20rounded%22%7D%2C%7B%22id%22%3A%22grid-item-5%22%2C%22content%22%3A%22Grid%20Item%205%22%2C%22colSpan%22%3A1%2C%22rowSpan%22%3A1%2C%22colStart%22%3A3%2C%22rowStart%22%3A2%2C%22className%22%3A%22bg-red-100%20p-4%20border%20rounded%22%7D%5D%2C%22dataColumns%22%3A%5B%22UserId%22%5D%2C%22columnLookups%22%3A%7B%22UserId%22%3A%7B%22sourceId%22%3A%22source_gq_0q1dz%22%2C%22path%22%3A%22%22%2C%22valueField%22%3A%22Id%22%2C%22textField%22%3A%22UserName%22%7D%7D%2C%22columnCaptions%22%3A%7B%7D%7D%2C%22events%22%3A%7B%7D%2C%22bindings%22%3A%7B%22items%22%3A%7B%22sourceId%22%3A%22source_tq_veb4m%22%2C%22path%22%3A%22%22%7D%7D%2C%22children%22%3A%5B%5D%2C%22ref%22%3A%22grid1%22%7D%2C%7B%22id%22%3A%22cmp_mt0288i3_ebpj5%22%2C%22type%22%3A%22ListView%22%2C%22kind%22%3A%22platform%22%2C%22props%22%3A%7B%22listFormCode%22%3A%22AbpIdentity.Roles%22%2C%22height%22%3A%22520px%22%2C%22filters%22%3A%5B%7B%22id%22%3A%22filter_y9_tr73w%22%2C%22field%22%3A%22Id%22%2C%22operator%22%3A%22eq%22%2C%22source%22%3A%22record%22%2C%22value%22%3A%22form1.Id%22%2C%22previewValue%22%3A%22%22%2C%22required%22%3Atrue%7D%5D%2C%22className%22%3A%22mt-4%22%7D%2C%22events%22%3A%7B%7D%2C%22bindings%22%3A%7B%7D%2C%22children%22%3A%5B%5D%2C%22ref%22%3A%22listView1%22%7D%5D%2C%22canvas%22%3A%7B%22width%22%3A%22responsive%22%7D%2C%22lifecycle%22%3A%7B%22onMount%22%3A%22%22%7D%2C%22dataSources%22%3A%5B%7B%22id%22%3A%22source_5w_5ibpv%22%2C%22name%22%3A%22AbpRoles%20%C2%B7%20GetList%22%2C%22method%22%3A%22GET%22%2C%22url%22%3A%22%2Fapi%2Fapp%2Fcrudendpoint%2FAbpRoles%22%2C%22responsePath%22%3A%22%22%7D%2C%7B%22id%22%3A%22source_i7_msox4%22%2C%22name%22%3A%22AbpRoles%20%C2%B7%20GetById%22%2C%22method%22%3A%22GET%22%2C%22url%22%3A%22%2Fapi%2Fapp%2Fcrudendpoint%2FAbpRoles%2F%7Bid%7D%22%2C%22responsePath%22%3A%22%22%7D%2C%7B%22id%22%3A%22source_0t_avok9%22%2C%22name%22%3A%22AbpRoles%20%C2%B7%20Delete%22%2C%22method%22%3A%22DELETE%22%2C%22url%22%3A%22%2Fapi%2Fapp%2Fcrudendpoint%2FAbpRoles%2F%7Bid%7D%22%2C%22responsePath%22%3A%22%22%7D%2C%7B%22id%22%3A%22source_h3_484qa%22%2C%22name%22%3A%22AbpRoles%20%C2%B7%20Create%22%2C%22method%22%3A%22POST%22%2C%22url%22%3A%22%2Fapi%2Fapp%2Fcrudendpoint%2FAbpRoles%22%2C%22responsePath%22%3A%22%22%7D%2C%7B%22id%22%3A%22source_gb_d9cvs%22%2C%22name%22%3A%22AbpRoles%20%C2%B7%20Update%22%2C%22method%22%3A%22PUT%22%2C%22url%22%3A%22%2Fapi%2Fapp%2Fcrudendpoint%2FAbpRoles%2F%7Bid%7D%22%2C%22responsePath%22%3A%22%22%7D%2C%7B%22id%22%3A%22source_tq_veb4m%22%2C%22name%22%3A%22AbpUserRoles%20%C2%B7%20GetList%22%2C%22method%22%3A%22GET%22%2C%22url%22%3A%22%2Fapi%2Fapp%2Fcrudendpoint%2FAbpUserRoles%22%2C%22responsePath%22%3A%22%22%2C%22filters%22%3A%5B%7B%22id%22%3A%22filter_sw_86e0q%22%2C%22field%22%3A%22RoleId%22%2C%22operator%22%3A%22eq%22%2C%22source%22%3A%22record%22%2C%22value%22%3A%22form1.id%22%2C%22previewValue%22%3A%22%22%2C%22required%22%3Atrue%7D%5D%7D%2C%7B%22id%22%3A%22source_d2_bri5s%22%2C%22name%22%3A%22AbpUserRoles%20%C2%B7%20GetById%22%2C%22method%22%3A%22GET%22%2C%22url%22%3A%22%2Fapi%2Fapp%2Fcrudendpoint%2FAbpUserRoles%2F%7Bid%7D%22%2C%22responsePath%22%3A%22%22%7D%2C%7B%22id%22%3A%22source_gq_0q1dz%22%2C%22name%22%3A%22AbpUsers%20%C2%B7%20GetList%22%2C%22method%22%3A%22GET%22%2C%22url%22%3A%22%2Fapi%2Fapp%2Fcrudendpoint%2FAbpUsers%22%2C%22responsePath%22%3A%22%22%7D%2C%7B%22id%22%3A%22source_u8_nu1lu%22%2C%22name%22%3A%22AbpUsers%20%C2%B7%20GetById%22%2C%22method%22%3A%22GET%22%2C%22url%22%3A%22%2Fapi%2Fapp%2Fcrudendpoint%2FAbpUsers%2F%7Bid%7D%22%2C%22responsePath%22%3A%22%22%7D%5D%2C%22permissionCode%22%3A%22%22%7D__*/\nconst RoleComponent = () => {\n const getByPath = (value, path) => {\n if (!path) return value\n const readPath = (target, targetPath) => targetPath.split('.').filter(Boolean).reduce((current, key) => current?.[key], target)\n const directValue = readPath(value, path)\n if (directValue !== undefined) return directValue\n if (path.startsWith('result.')) return readPath(value, path.slice('result.'.length))\n return value?.result === undefined ? undefined : readPath(value.result, path)\n }\n\n const resolveLookupText = (lookup, value) => {\n if (!lookup || value === null || value === undefined || value === \"\") return undefined\n const rows = getByPath(lookup.rows, lookup.path)\n if (!Array.isArray(rows)) return undefined\n const key = String(value)\n const row = rows.find((item) => item && typeof item === \"object\" && String(getByPath(item, lookup.value) ?? \"\") === key)\n if (!row) return undefined\n const text = getByPath(row, lookup.text)\n return text === null || text === undefined || text === \"\" ? undefined : text\n }\n\n const localeCulture = () => {\n const injected = typeof getCulture === \"function\" ? getCulture() : \"\"\n if (injected) return injected\n if (typeof document !== \"undefined\" && document.documentElement.lang) return document.documentElement.lang\n return undefined\n }\n // Patterns come from the culture itself; a fixed day/month/year order would\n // print the same layout in every language.\n const localeDateOptions = { dateStyle: \"short\" }\n const localeTimeOptions = { dateStyle: \"short\", timeStyle: \"short\" }\n const localeIsoPattern = /^(\\d{4})-(\\d{2})-(\\d{2})(?:[T ](\\d{2}):(\\d{2})(?::(\\d{2})(?:\\.\\d+)?)?(Z|[+-]\\d{2}:?\\d{2})?)?$/\n const formatLocaleValue = (value) => {\n if (value === null || value === undefined) return \"\"\n const culture = localeCulture()\n if (typeof value === \"number\") {\n if (!Number.isFinite(value)) return String(value)\n // Grouping and the decimal separator are the culture's decision; only the\n // precision guard stays, so a long decimal is not rounded away.\n return new Intl.NumberFormat(culture, { maximumFractionDigits: 20 }).format(value)\n }\n if (value instanceof Date) {\n return Number.isNaN(value.getTime()) ? \"\" : new Intl.DateTimeFormat(culture, localeTimeOptions).format(value)\n }\n if (typeof value !== \"string\") return String(value)\n const parts = localeIsoPattern.exec(value.trim())\n if (!parts) return value\n const parsed = new Date(value)\n if (Number.isNaN(parsed.getTime())) return value\n if (parts[4] === undefined) {\n const day = new Date(Number(parts[1]), Number(parts[2]) - 1, Number(parts[3]))\n return new Intl.DateTimeFormat(culture, localeDateOptions).format(day)\n }\n return new Intl.DateTimeFormat(culture, localeTimeOptions).format(parsed)\n }\n\n const readSqlField = (record, field) => {\n if (!record || typeof record !== \"object\" || Array.isArray(record)) return undefined\n if (field in record) return record[field]\n const matched = Object.keys(record).find((key) => key.toLowerCase() === String(field).toLowerCase())\n return matched === undefined ? undefined : record[matched]\n }\n const toSqlRows = (value, collectionPath) => {\n const source = collectionPath ? getByPath(value, collectionPath) : value\n const rows = Array.isArray(source)\n ? source\n : source && typeof source === \"object\"\n ? (Array.isArray(source.items) ? source.items : [source])\n : []\n return rows.filter((item) => item && typeof item === \"object\" && !Array.isArray(item))\n }\n const setSqlField = (record, path, value) => {\n const keys = String(path || \"\").split(\".\").filter(Boolean)\n if (!keys.length) return record\n const next = { ...(record || {}) }\n let target = next\n for (let index = 0; index < keys.length - 1; index += 1) {\n const child = target[keys[index]]\n target[keys[index]] = child && typeof child === \"object\" && !Array.isArray(child) ? { ...child } : {}\n target = target[keys[index]]\n }\n target[keys[keys.length - 1]] = value\n return next\n }\n const bindSqlUrl = (url, record, keyField) => {\n let keyBound = false\n const boundUrl = String(url).replace(/\\{([^}]+)\\}|(?<=\\/):([A-Za-z_][A-Za-z0-9_]*)/g, (match, braced, colon) => {\n const name = (braced || colon || \"\").trim()\n if (!name) return match\n const value = readSqlField(record, name) ?? readSqlField(record, keyField)\n if (value === undefined || value === null || value === \"\") return match\n if (name.toLowerCase() === String(keyField).toLowerCase()) keyBound = true\n return encodeURIComponent(String(value))\n })\n return { url: boundUrl, keyBound }\n }\n const appendQueryParam = (url, name, value) =>\n url + (url.includes(\"?\") ? \"&\" : \"?\") + encodeURIComponent(name) + \"=\" + encodeURIComponent(value)\n const hasSqlUrlParams = (url) => /\\{[^}]+\\}|(?<=\\/):[A-Za-z_][A-Za-z0-9_]*/.test(String(url))\n const readUrlKey = (source, name) => {\n if (typeof window === \"undefined\") return \"\"\n if (source === \"query\") return new URLSearchParams(window.location.search).get(name) ?? \"\"\n const segments = window.location.pathname.split(\"/\").filter(Boolean)\n return segments.length ? decodeURIComponent(segments[segments.length - 1]) : \"\"\n }\n const filterSqlRowsByKey = (rows, keyField, keyValue) => {\n if (!keyValue) return rows\n const matched = rows.filter((row) => String(readSqlField(row, keyField) ?? \"\") === String(keyValue))\n // No match means the endpoint already filtered server side, or the key column\n // is named differently — keeping the rows beats showing an empty form.\n return matched.length ? matched : rows\n }\n // Update payload: only the columns the user actually edited, plus the key.\n const toSqlChanges = (original, current, keyField) => {\n const changes = {}\n const isSame = (left, right) => JSON.stringify(left ?? null) === JSON.stringify(right ?? null)\n Object.keys(current || {}).forEach((column) => {\n if (!isSame(original ? original[column] : undefined, current[column])) changes[column] = current[column]\n })\n const keyColumn = Object.keys(current || {}).find((column) => column.toLowerCase() === String(keyField).toLowerCase())\n if (keyColumn !== undefined && changes[keyColumn] === undefined) changes[keyColumn] = current[keyColumn]\n return changes\n }\n const callSqlEndpoint = (target, record, keyField, payload) => {\n // The URL is always bound from the full record: a placeholder may reference a\n // column that the update payload does not carry.\n const bound = bindSqlUrl(target.url, record, keyField)\n const request = { url: bound.url, method: target.method }\n if (target.method === \"POST\" || target.method === \"PUT\") request.data = payload === undefined ? record : payload\n const keyValue = readSqlField(record, keyField)\n const hasKey = keyValue !== undefined && keyValue !== null && keyValue !== \"\"\n // The key still has to reach the endpoint when the URL carries no placeholder.\n if (!bound.keyBound && hasKey && target.method !== \"POST\") request.params = { [keyField]: keyValue }\n return apiService.fetchData(request)\n }\n // A new record starts at the top of the form: the first enabled editor takes the\n // cursor, so a Yeni click is immediately followed by typing.\n const focusFirstSqlField = (host) => {\n if (typeof window === \"undefined\") return\n window.requestAnimationFrame(() => {\n const field = host.current?.querySelector(\"input:not([type='hidden']):not([disabled]):not([readonly]), textarea:not([disabled]):not([readonly]), select:not([disabled])\")\n if (!field) return\n field.focus()\n if (typeof field.select === \"function\") field.select()\n })\n }\n // Defaults declared on the bound children; only a new record starts from them.\n const buildSqlDefaults = (fields) => fields.reduce((record, field) => setSqlField(record, field.path, resolveSqlDefault(field)), {})\n const resolveSqlDefault = (field) => {\n if (field.token !== \"today\" && field.token !== \"now\") return field.value\n const now = new Date()\n const pad = (value) => String(value).padStart(2, \"0\")\n const day = now.getFullYear() + \"-\" + pad(now.getMonth() + 1) + \"-\" + pad(now.getDate())\n if (field.token === \"today\" || field.dateOnly) return day\n return day + \"T\" + pad(now.getHours()) + \":\" + pad(now.getMinutes()) + \":\" + pad(now.getSeconds())\n }\n const toSqlErrorMessage = (error) => error?.response?.data?.error?.message || error?.response?.data?.message || error?.message || translate(\"::App.Platform.IslemBasarisiz\")\n\n // Value of a filter that reads the page URL: a query string parameter, or a\n // path segment — the one after a segment named like the parameter, otherwise\n // the last one, which is the /orders/{id} detail page shape.\n const readDesignerUrlValue = (kind, name) => {\n if (typeof window === \"undefined\") return \"\"\n if (kind === \"query\") return new URLSearchParams(window.location.search).get(name) ?? \"\"\n const segments = window.location.pathname.split(\"/\").filter(Boolean)\n if (!segments.length) return \"\"\n const index = name ? segments.findIndex((segment) => segment.toLowerCase() === String(name).toLowerCase()) : -1\n if (index >= 0 && index < segments.length - 1) return decodeURIComponent(segments[index + 1])\n return decodeURIComponent(segments[segments.length - 1])\n }\n // Filters are appended as query parameters: `Column` for an equality and\n // `Column.operator` for anything else, which is what the endpoint parses.\n // Returns \"\" when a required filter has no value yet — the caller must skip the\n // request instead of loading the unfiltered collection.\n const buildFilteredUrl = (url, filters) => {\n const parts = []\n for (const filter of filters || []) {\n const value = filter.value === undefined || filter.value === null ? \"\" : String(filter.value)\n if (!value) {\n if (filter.required) return \"\"\n continue\n }\n parts.push(encodeURIComponent(filter.param) + \"=\" + encodeURIComponent(value))\n }\n if (!parts.length) return url\n return url + (url.includes(\"?\") ? \"&\" : \"?\") + parts.join(\"&\")\n }\n\n const buildPlatformFilterParams = (filters) => {\n const expressions = []\n for (const filter of filters || []) {\n if (filter.kind === \"isnull\" || filter.kind === \"notnull\") {\n expressions.push([filter.field, filter.operator, null])\n continue\n }\n const value = filter.value === undefined || filter.value === null ? \"\" : String(filter.value)\n if (!value) {\n if (filter.required) return null\n continue\n }\n if (filter.kind === \"in\") {\n const entries = value.split(\",\").map((entry) => entry.trim()).filter(Boolean).map((entry) => [filter.field, \"=\", entry])\n if (!entries.length) continue\n expressions.push(entries.reduce((result, entry) => (result ? [result, \"or\", entry] : entry), null))\n continue\n }\n expressions.push([filter.field, filter.operator, value])\n }\n const params = new URLSearchParams()\n const merged = expressions.reduce((result, expression) => (result ? [result, \"and\", expression] : expression), null)\n if (merged) params.set(\"filter\", JSON.stringify(merged))\n return params\n }\n\n const [data_source_5w_5ibpv, setData_source_5w_5ibpv] = React.useState(null)\n const [data_source_i7_msox4, setData_source_i7_msox4] = React.useState(null)\n const [data_source_0t_avok9] = React.useState(null)\n const [data_source_h3_484qa] = React.useState(null)\n const [data_source_gb_d9cvs] = React.useState(null)\n const [data_source_tq_veb4m, setData_source_tq_veb4m] = React.useState(null)\n const [data_source_d2_bri5s, setData_source_d2_bri5s] = React.useState(null)\n const [data_source_gq_0q1dz, setData_source_gq_0q1dz] = React.useState(null)\n const [data_source_u8_nu1lu, setData_source_u8_nu1lu] = React.useState(null)\n\n const sqlselect_cmp_msyfmegt_jkkn3 = { url: \"/api/app/crudendpoint/AbpRoles\", method: \"GET\", responsePath: \"\" }\n const sqlinsert_cmp_msyfmegt_jkkn3 = { url: \"/api/app/crudendpoint/AbpRoles\", method: \"POST\", responsePath: \"\" }\n const sqlupdate_cmp_msyfmegt_jkkn3 = { url: \"/api/app/crudendpoint/AbpRoles/{id}\", method: \"PUT\", responsePath: \"\" }\n const sqldelete_cmp_msyfmegt_jkkn3 = { url: \"/api/app/crudendpoint/AbpRoles/{id}\", method: \"DELETE\", responsePath: \"\" }\n const sqlCanSelect_cmp_msyfmegt_jkkn3 = true\n const sqlCanInsert_cmp_msyfmegt_jkkn3 = true\n const sqlCanUpdate_cmp_msyfmegt_jkkn3 = true\n const sqlCanDelete_cmp_msyfmegt_jkkn3 = true\n const sqlKey_cmp_msyfmegt_jkkn3 = \"id\"\n const sqlKeyParam_cmp_msyfmegt_jkkn3 = \"id\"\n const [data_cmp_msyfmegt_jkkn3, setData_cmp_msyfmegt_jkkn3] = React.useState({})\n const [sqlOriginal_cmp_msyfmegt_jkkn3, setSqlOriginal_cmp_msyfmegt_jkkn3] = React.useState({})\n const [sqlMode_cmp_msyfmegt_jkkn3, setSqlMode_cmp_msyfmegt_jkkn3] = React.useState(\"edit\")\n const [sqlIndex_cmp_msyfmegt_jkkn3, setSqlIndex_cmp_msyfmegt_jkkn3] = React.useState(0)\n const [sqlBusy_cmp_msyfmegt_jkkn3, setSqlBusy_cmp_msyfmegt_jkkn3] = React.useState(false)\n const [sqlError_cmp_msyfmegt_jkkn3, setSqlError_cmp_msyfmegt_jkkn3] = React.useState(\"\")\n // Container element, so New can hand the cursor to the first field of the form.\n const sqlHost_cmp_msyfmegt_jkkn3 = React.useRef(null)\n // The Select key can come from the page URL, which is how a detail page reads\n // /api/app/orders/{id} or /api/app/orders?id=… for a single record.\n const sqlUrlKey_cmp_msyfmegt_jkkn3 = readUrlKey(\"query\", sqlKeyParam_cmp_msyfmegt_jkkn3)\n const sqlSelectUrl_cmp_msyfmegt_jkkn3 = React.useMemo(() => {\n if (!sqlselect_cmp_msyfmegt_jkkn3) return \"\"\n const base = sqlUrlKey_cmp_msyfmegt_jkkn3\n ? (() => {\n const bound = bindSqlUrl(sqlselect_cmp_msyfmegt_jkkn3.url, { [sqlKeyParam_cmp_msyfmegt_jkkn3]: sqlUrlKey_cmp_msyfmegt_jkkn3 }, sqlKeyParam_cmp_msyfmegt_jkkn3)\n return bound.keyBound ? bound.url : appendQueryParam(bound.url, sqlKeyParam_cmp_msyfmegt_jkkn3, sqlUrlKey_cmp_msyfmegt_jkkn3)\n })()\n : sqlselect_cmp_msyfmegt_jkkn3.url\n return base\n }, [sqlUrlKey_cmp_msyfmegt_jkkn3])\n // A key in the page URL always narrows the result: a list endpoint ignores the\n // parameter server side, so the requested record is picked out here. Without a\n // key every row is kept and the navigation below takes over.\n const sqlRows_cmp_msyfmegt_jkkn3 = React.useMemo(\n () => filterSqlRowsByKey(toSqlRows(data_source_5w_5ibpv, \"\"), sqlKey_cmp_msyfmegt_jkkn3, sqlUrlKey_cmp_msyfmegt_jkkn3),\n [data_source_5w_5ibpv, sqlUrlKey_cmp_msyfmegt_jkkn3],\n )\n // A fresh result set resets the position; New mode is left untouched so an\n // unsaved draft is not overwritten by a re-render.\n React.useEffect(() => {\n setSqlIndex_cmp_msyfmegt_jkkn3((current) => (current < sqlRows_cmp_msyfmegt_jkkn3.length ? current : 0))\n }, [sqlRows_cmp_msyfmegt_jkkn3])\n React.useEffect(() => {\n const row = sqlRows_cmp_msyfmegt_jkkn3[sqlIndex_cmp_msyfmegt_jkkn3]\n if (!row) return\n setData_cmp_msyfmegt_jkkn3(row)\n setSqlOriginal_cmp_msyfmegt_jkkn3(row)\n setSqlMode_cmp_msyfmegt_jkkn3(\"edit\")\n }, [sqlRows_cmp_msyfmegt_jkkn3, sqlIndex_cmp_msyfmegt_jkkn3])\n const sqlHasKey_cmp_msyfmegt_jkkn3 = (() => {\n const value = readSqlField(data_cmp_msyfmegt_jkkn3, sqlKey_cmp_msyfmegt_jkkn3)\n return value !== undefined && value !== null && value !== \"\"\n })()\n const sqlSetField_cmp_msyfmegt_jkkn3 = React.useCallback((path, value) => setData_cmp_msyfmegt_jkkn3((current) => setSqlField(current, path, value)), [])\n const sqlReload_cmp_msyfmegt_jkkn3 = React.useCallback(async () => {\n // Without the Read permission the container is not rendered at all; the\n // request is dropped here so it is not fired for a form nobody can see.\n if (!sqlCanSelect_cmp_msyfmegt_jkkn3) return\n if (!sqlSelectUrl_cmp_msyfmegt_jkkn3) return\n // Calling a URL that still holds a placeholder is a guaranteed 400, so the\n // missing key is reported in the component instead.\n if (hasSqlUrlParams(sqlSelectUrl_cmp_msyfmegt_jkkn3)) {\n throw new Error(translate(\"::App.Form.MissingKeyParam\", { 0: sqlKeyParam_cmp_msyfmegt_jkkn3 }))\n }\n const response = await apiService.fetchData({ url: sqlSelectUrl_cmp_msyfmegt_jkkn3, method: \"GET\" })\n setData_source_5w_5ibpv(getByPath(response.data, sqlselect_cmp_msyfmegt_jkkn3.responsePath))\n }, [sqlSelectUrl_cmp_msyfmegt_jkkn3])\n // Reload wrapped with the busy/error handling the toolbar and mount effect need,\n // so a failing Select is reported in the component instead of the console.\n const sqlRefresh_cmp_msyfmegt_jkkn3 = React.useCallback(async () => {\n setSqlBusy_cmp_msyfmegt_jkkn3(true)\n setSqlError_cmp_msyfmegt_jkkn3(\"\")\n try {\n await sqlReload_cmp_msyfmegt_jkkn3()\n } catch (error) {\n setSqlError_cmp_msyfmegt_jkkn3(toSqlErrorMessage(error))\n } finally {\n setSqlBusy_cmp_msyfmegt_jkkn3(false)\n }\n }, [sqlReload_cmp_msyfmegt_jkkn3])\n const sqlNew_cmp_msyfmegt_jkkn3 = React.useCallback(() => {\n if (!sqlCanInsert_cmp_msyfmegt_jkkn3) return\n setSqlError_cmp_msyfmegt_jkkn3(\"\")\n // Defaults are resolved on every New, so an @today column carries the day the\n // record was opened rather than the day the component was generated.\n const sqlDraft_cmp_msyfmegt_jkkn3 = buildSqlDefaults([{\"path\":\"IsDefault\",\"value\":true},{\"path\":\"IsStatic\",\"value\":true},{\"path\":\"IsPublic\",\"value\":false},{\"path\":\"EntityVersion\",\"value\":3},{\"path\":\"ExtraProperties\",\"value\":\"{}\"}])\n setData_cmp_msyfmegt_jkkn3(sqlDraft_cmp_msyfmegt_jkkn3)\n setSqlOriginal_cmp_msyfmegt_jkkn3({})\n setSqlMode_cmp_msyfmegt_jkkn3(\"new\")\n // After the re-render that paints the empty form, not before it.\n focusFirstSqlField(sqlHost_cmp_msyfmegt_jkkn3)\n }, [])\n const sqlPrev_cmp_msyfmegt_jkkn3 = React.useCallback(() => setSqlIndex_cmp_msyfmegt_jkkn3((current) => Math.max(0, current - 1)), [])\n const sqlNext_cmp_msyfmegt_jkkn3 = React.useCallback(() => setSqlIndex_cmp_msyfmegt_jkkn3((current) => Math.min(sqlRows_cmp_msyfmegt_jkkn3.length - 1, current + 1)), [sqlRows_cmp_msyfmegt_jkkn3])\n const sqlSave_cmp_msyfmegt_jkkn3 = async () => {\n const isNew = sqlMode_cmp_msyfmegt_jkkn3 === \"new\"\n const target = isNew ? sqlinsert_cmp_msyfmegt_jkkn3 : sqlupdate_cmp_msyfmegt_jkkn3\n // The button is already hidden without the permission; a script calling save\n // through a ref must not be able to walk around it either.\n if (!(isNew ? sqlCanInsert_cmp_msyfmegt_jkkn3 : sqlCanUpdate_cmp_msyfmegt_jkkn3)) return\n if (!target) {\n setSqlError_cmp_msyfmegt_jkkn3(isNew ? translate(\"::App.Form.InsertEndpointMissing\") : translate(\"::App.Form.UpdateEndpointMissing\"))\n return\n }\n // Update carries only the edited columns plus the key; Insert sends the record.\n const payload = isNew ? data_cmp_msyfmegt_jkkn3 : toSqlChanges(sqlOriginal_cmp_msyfmegt_jkkn3, data_cmp_msyfmegt_jkkn3, sqlKey_cmp_msyfmegt_jkkn3)\n if (!isNew && Object.keys(payload).filter((column) => column.toLowerCase() !== String(sqlKey_cmp_msyfmegt_jkkn3).toLowerCase()).length === 0) {\n setSqlError_cmp_msyfmegt_jkkn3(translate(\"::App.Form.NoChanges\"))\n return\n }\n setSqlBusy_cmp_msyfmegt_jkkn3(true)\n setSqlError_cmp_msyfmegt_jkkn3(\"\")\n try {\n const response = await callSqlEndpoint(target, data_cmp_msyfmegt_jkkn3, sqlKey_cmp_msyfmegt_jkkn3, payload)\n // With a Select endpoint the reload is the source of truth and its effect\n // repopulates the form; without one, the response row is all there is.\n if (sqlselect_cmp_msyfmegt_jkkn3) {\n await sqlReload_cmp_msyfmegt_jkkn3()\n } else {\n const saved = toSqlRows(response?.data, \"\")[0]\n if (saved) {\n setData_cmp_msyfmegt_jkkn3(saved)\n setSqlOriginal_cmp_msyfmegt_jkkn3(saved)\n }\n }\n setSqlMode_cmp_msyfmegt_jkkn3(\"edit\")\n } catch (error) {\n setSqlError_cmp_msyfmegt_jkkn3(toSqlErrorMessage(error))\n } finally {\n setSqlBusy_cmp_msyfmegt_jkkn3(false)\n }\n }\n const sqlDelete_cmp_msyfmegt_jkkn3 = async () => {\n if (!sqlCanDelete_cmp_msyfmegt_jkkn3 || !sqldelete_cmp_msyfmegt_jkkn3 || !sqlHasKey_cmp_msyfmegt_jkkn3 || sqlMode_cmp_msyfmegt_jkkn3 === \"new\") return\n // Deleting cannot be undone from the form, so it always asks first.\n const confirmMessage = translate(\"::App.Form.DeleteConfirm\", { 0: sqlKey_cmp_msyfmegt_jkkn3 + \" = \" + readSqlField(data_cmp_msyfmegt_jkkn3, sqlKey_cmp_msyfmegt_jkkn3) })\n if (typeof window !== \"undefined\" && !window.confirm(confirmMessage)) return\n setSqlBusy_cmp_msyfmegt_jkkn3(true)\n setSqlError_cmp_msyfmegt_jkkn3(\"\")\n try {\n await callSqlEndpoint(sqldelete_cmp_msyfmegt_jkkn3, data_cmp_msyfmegt_jkkn3, sqlKey_cmp_msyfmegt_jkkn3)\n setData_cmp_msyfmegt_jkkn3({})\n setSqlOriginal_cmp_msyfmegt_jkkn3({})\n setSqlMode_cmp_msyfmegt_jkkn3(\"new\")\n await sqlReload_cmp_msyfmegt_jkkn3()\n } catch (error) {\n setSqlError_cmp_msyfmegt_jkkn3(toSqlErrorMessage(error))\n } finally {\n setSqlBusy_cmp_msyfmegt_jkkn3(false)\n }\n }\n React.useEffect(() => { void sqlRefresh_cmp_msyfmegt_jkkn3() }, [sqlRefresh_cmp_msyfmegt_jkkn3])\n\n const filters_source_tq_veb4m = [{ param: \"RoleId\", value: readSqlField(data_cmp_msyfmegt_jkkn3, \"id\"), required: true }]\n const filterKey_source_tq_veb4m = JSON.stringify(filters_source_tq_veb4m)\n React.useEffect(() => {\n const url = buildFilteredUrl(\"/api/app/crudendpoint/AbpUserRoles\", filters_source_tq_veb4m)\n // An unmet required filter leaves the component empty on purpose.\n if (!url) {\n setData_source_tq_veb4m(null)\n return\n }\n let active = true\n apiService.fetchData({ url, method: 'GET' }).then((response) => {\n if (active) setData_source_tq_veb4m(getByPath(response.data, \"\"))\n }).catch((error) => console.error(\"AbpUserRoles · GetList endpoint error:\", error))\n return () => { active = false }\n }, [filterKey_source_tq_veb4m])\n\n React.useEffect(() => {\n let active = true\n apiService.fetchData({ url: \"/api/app/crudendpoint/AbpUsers\", method: 'GET' }).then((response) => {\n if (active) setData_source_gq_0q1dz(getByPath(response.data, \"\"))\n }).catch((error) => console.error(\"AbpUsers · GetList endpoint error:\", error))\n return () => { active = false }\n }, [])\n\n const platformFilters_cmp_mt0288i3_ebpj5 = [{ field: \"Id\", operator: \"=\", kind: \"eq\", value: readSqlField(data_cmp_msyfmegt_jkkn3, \"Id\"), required: true }]\n const platformKey_cmp_mt0288i3_ebpj5 = JSON.stringify(platformFilters_cmp_mt0288i3_ebpj5)\n // The views keep the params object in their own state and write back to it, so\n // it must stay the same object until a filter value actually changes.\n const platformParams_cmp_mt0288i3_ebpj5 = React.useMemo(() => buildPlatformFilterParams(platformFilters_cmp_mt0288i3_ebpj5), [platformKey_cmp_mt0288i3_ebpj5])\n\n const handle_cmp_msyfpfia_ifhdq_onChange = (valueOrEvent, originalEvent) => {\n sqlSetField_cmp_msyfmegt_jkkn3(\"Name\", valueOrEvent?.target?.value ?? valueOrEvent)\n const event = valueOrEvent\n }\n\n const handle_cmp_msyjl707_crcn7_onChange = (valueOrEvent, originalEvent) => {\n sqlSetField_cmp_msyfmegt_jkkn3(\"NormalizedName\", valueOrEvent?.target?.value ?? valueOrEvent)\n const event = valueOrEvent\n }\n\n const handle_cmp_msyjdodn_u3o74_onChange = (valueOrEvent, originalEvent) => {\n sqlSetField_cmp_msyfmegt_jkkn3(\"ConcurrencyStamp\", valueOrEvent?.target?.value ?? valueOrEvent)\n const event = valueOrEvent\n }\n\n const handle_cmp_msyje9ys_2c08a_onChange = (valueOrEvent, originalEvent) => {\n sqlSetField_cmp_msyfmegt_jkkn3(\"IsDefault\", typeof originalEvent === \"object\" && originalEvent?.target ? Boolean(originalEvent.target.checked) : typeof valueOrEvent === \"object\" && valueOrEvent?.target ? Boolean(valueOrEvent.target.checked) : Boolean(valueOrEvent))\n const event = { checked: Boolean(valueOrEvent), originalEvent, target: originalEvent?.target }\n }\n\n const handle_cmp_msyjebu2_27tqi_onChange = (valueOrEvent, originalEvent) => {\n sqlSetField_cmp_msyfmegt_jkkn3(\"IsStatic\", typeof originalEvent === \"object\" && originalEvent?.target ? Boolean(originalEvent.target.checked) : typeof valueOrEvent === \"object\" && valueOrEvent?.target ? Boolean(valueOrEvent.target.checked) : Boolean(valueOrEvent))\n const event = { checked: Boolean(valueOrEvent), originalEvent, target: originalEvent?.target }\n }\n\n const handle_cmp_msyjmetn_gojol_onChange = (valueOrEvent, originalEvent) => {\n sqlSetField_cmp_msyfmegt_jkkn3(\"IsPublic\", typeof originalEvent === \"object\" && originalEvent?.target ? Boolean(originalEvent.target.checked) : typeof valueOrEvent === \"object\" && valueOrEvent?.target ? Boolean(valueOrEvent.target.checked) : Boolean(valueOrEvent))\n const event = { checked: Boolean(valueOrEvent), originalEvent, target: originalEvent?.target }\n }\n\n const handle_cmp_msyjo0g8_oathk_onChange = (valueOrEvent, originalEvent) => {\n sqlSetField_cmp_msyfmegt_jkkn3(\"EntityVersion\", valueOrEvent?.target?.value ?? valueOrEvent)\n const event = valueOrEvent\n }\n\n const handle_cmp_msyjplfq_py8lz_onChange = (valueOrEvent, originalEvent) => {\n sqlSetField_cmp_msyfmegt_jkkn3(\"ExtraProperties\", valueOrEvent?.target?.value ?? valueOrEvent)\n const event = valueOrEvent\n }\n\n return (\n <>\n {sqlCanSelect_cmp_msyfmegt_jkkn3 ? (\n {sqlHasKey_cmp_msyfmegt_jkkn3 ? sqlKey_cmp_msyfmegt_jkkn3 + \": \" + readSqlField(data_cmp_msyfmegt_jkkn3, sqlKey_cmp_msyfmegt_jkkn3) : sqlMode_cmp_msyfmegt_jkkn3 === \"new\" ? translate(\"::App.Platform.NewRecord\") : \"\"}}>\n
\n
\n \n \n \n \n {\"Is Default\"}\n \n \n {\"Is Static\"}\n \n \n {\"Is Public\"}\n \n \n \n
\n
\n {sqlRows_cmp_msyfmegt_jkkn3.length > 1 ? (\n <>\n \n {`${sqlIndex_cmp_msyfmegt_jkkn3 + 1} / ${sqlRows_cmp_msyfmegt_jkkn3.length}`}\n \n \n \n ) : null}\n {sqlinsert_cmp_msyfmegt_jkkn3 && sqlCanInsert_cmp_msyfmegt_jkkn3 ? : null}\n {(sqlMode_cmp_msyfmegt_jkkn3 === \"new\" ? sqlinsert_cmp_msyfmegt_jkkn3 && sqlCanInsert_cmp_msyfmegt_jkkn3 : sqlupdate_cmp_msyfmegt_jkkn3 && sqlCanUpdate_cmp_msyfmegt_jkkn3) ? : null}\n {sqldelete_cmp_msyfmegt_jkkn3 && sqlCanDelete_cmp_msyfmegt_jkkn3 ? : null}\n {sqlselect_cmp_msyfmegt_jkkn3 && sqlCanSelect_cmp_msyfmegt_jkkn3 ? : null}\n {sqlMode_cmp_msyfmegt_jkkn3 === \"new\" ? translate(\"::App.Platform.NewRecord\") : translate(\"::App.Platform.Editing\")}\n
\n {sqlError_cmp_msyfmegt_jkkn3 ?
{sqlError_cmp_msyfmegt_jkkn3}
: null}\n
\n
\n ) : null}\n
\n {(() => {\n const gridItems_cmp_mt023r5u_rb2jv = (Array.isArray(getByPath(data_source_tq_veb4m, \"\")) ? getByPath(data_source_tq_veb4m, \"\") : Array.isArray(data_source_tq_veb4m) ? data_source_tq_veb4m : null)\n // Nothing loaded yet — an endpoint still in flight, or a required filter\n // holding the request back — is an empty grid, not a binding error.\n if (gridItems_cmp_mt023r5u_rb2jv === null || gridItems_cmp_mt023r5u_rb2jv === undefined) return null\n if (!Array.isArray(gridItems_cmp_mt023r5u_rb2jv)) return
Grid items bağlantısı bir koleksiyon döndürmelidir.
\n const gridColumns_cmp_mt023r5u_rb2jv = [\"UserId\"]\n const gridLookups_cmp_mt023r5u_rb2jv = { \"UserId\": { rows: data_source_gq_0q1dz, path: \"\", value: \"Id\", text: \"UserName\" } }\n if (!gridColumns_cmp_mt023r5u_rb2jv.length) return
Preview için en az bir sütun seçin.
\n return (\n
\n \n \n {gridColumns_cmp_mt023r5u_rb2jv.map((column) => )}\n \n \n {gridItems_cmp_mt023r5u_rb2jv.map((gridItem_cmp_mt023r5u_rb2jv, rowIndex) => (\n \n {gridColumns_cmp_mt023r5u_rb2jv.map((column) => {\n const cellValue_cmp_mt023r5u_rb2jv = column === \"value\" ? gridItem_cmp_mt023r5u_rb2jv : getByPath(gridItem_cmp_mt023r5u_rb2jv, column)\n const lookedUp = resolveLookupText(gridLookups_cmp_mt023r5u_rb2jv[column], cellValue_cmp_mt023r5u_rb2jv)\n const cellText = lookedUp === undefined ? cellValue_cmp_mt023r5u_rb2jv : lookedUp\n const text = typeof cellText === \"object\" && cellText !== null && !(cellText instanceof Date) ? JSON.stringify(cellText) : formatLocaleValue(cellText) || \"—\"\n return \n })}\n \n ))}\n \n
{(column === \"value\" ? \"Value\" : column)}
{text}
\n
\n )\n })()}\n
\n {platformParams_cmp_mt0288i3_ebpj5 === null ? null : (\n \n )}\n \n )\n}\n\nexport default RoleComponent", - "Props": "{\"visualDesigner\":{\"version\":1,\"sourceMode\":\"visual\",\"nodes\":[{\"id\":\"cmp_msyfmegt_jkkn3\",\"type\":\"Form\",\"kind\":\"layout\",\"props\":{\"selectEndpoint\":\"source_5w_5ibpv\",\"insertEndpoint\":\"source_h3_484qa\",\"updateEndpoint\":\"source_gb_d9cvs\",\"deleteEndpoint\":\"source_0t_avok9\",\"keyFieldName\":\"id\",\"collectionPath\":\"\",\"keySource\":\"query\",\"keyParamName\":\"id\",\"previewKeyValue\":\"\",\"autoLoad\":true,\"showToolbar\":true,\"gap\":16,\"className\":\"\",\"title\":\"::AbpIdentity.Roles\",\"columnCount\":2,\"selectPermission\":\"\",\"insertPermission\":\"\"},\"events\":{\"onLoad\":\"\",\"onRecordChange\":\"\",\"onFieldChange\":\"\",\"onNewRecord\":\"\",\"onModeChange\":\"\",\"onBeforeSave\":\"\",\"onAfterSave\":\"\",\"onBeforeDelete\":\"\",\"onAfterDelete\":\"\",\"onError\":\"\"},\"bindings\":{},\"children\":[{\"id\":\"cmp_msyfpfia_ifhdq\",\"type\":\"Input\",\"kind\":\"ui\",\"props\":{\"asElement\":\"\",\"disabled\":false,\"invalid\":false,\"rows\":3,\"prefix\":\"::App.Platform.Name\",\"size\":\"md\",\"suffix\":\"\",\"textArea\":false,\"type\":\"text\",\"unstyle\":false,\"field\":\"\",\"form\":\"\",\"className\":\"\",\"children\":\"\",\"style\":{},\"placeholder\":\"Metin girin...\",\"value\":\"\",\"name\":\"\",\"autoComplete\":\"off\",\"autoFocus\":false,\"readOnly\":false,\"required\":false,\"maxLength\":\"\",\"minLength\":\"\",\"pattern\":\"\",\"sqlDefaultValue\":\"\"},\"events\":{\"onChange\":\"\"},\"bindings\":{\"value\":{\"sourceId\":\"cmp_msyfmegt_jkkn3\",\"path\":\"Name\"}},\"children\":[],\"ref\":\"input1\"},{\"id\":\"cmp_msyjl707_crcn7\",\"type\":\"Input\",\"kind\":\"ui\",\"props\":{\"asElement\":\"\",\"disabled\":false,\"invalid\":false,\"rows\":3,\"prefix\":\"Normalized Name :\",\"size\":\"md\",\"suffix\":\"\",\"textArea\":false,\"type\":\"text\",\"unstyle\":false,\"field\":\"\",\"form\":\"\",\"className\":\"\",\"children\":\"\",\"style\":{},\"placeholder\":\"Metin girin...\",\"value\":\"\",\"name\":\"\",\"autoComplete\":\"off\",\"autoFocus\":false,\"readOnly\":false,\"required\":false,\"maxLength\":\"\",\"minLength\":\"\",\"pattern\":\"\"},\"events\":{\"onChange\":\"\"},\"bindings\":{\"value\":{\"sourceId\":\"cmp_msyfmegt_jkkn3\",\"path\":\"NormalizedName\"}},\"children\":[],\"ref\":\"input2\"},{\"id\":\"cmp_msyjdodn_u3o74\",\"type\":\"Input\",\"kind\":\"ui\",\"props\":{\"asElement\":\"\",\"disabled\":false,\"invalid\":false,\"rows\":3,\"prefix\":\"Concurrent Stamp : \",\"size\":\"md\",\"suffix\":\"\",\"textArea\":false,\"type\":\"text\",\"unstyle\":false,\"field\":\"\",\"form\":\"\",\"className\":\"\",\"children\":\"\",\"style\":{},\"placeholder\":\"Metin girin...\",\"value\":\"\",\"name\":\"\",\"autoComplete\":\"off\",\"autoFocus\":false,\"readOnly\":false,\"required\":false,\"maxLength\":\"\",\"minLength\":\"\",\"pattern\":\"\"},\"events\":{\"onChange\":\"\"},\"bindings\":{\"value\":{\"sourceId\":\"cmp_msyfmegt_jkkn3\",\"path\":\"ConcurrencyStamp\"}},\"children\":[],\"ref\":\"input3\"},{\"id\":\"cmp_msyje9ys_2c08a\",\"type\":\"Checkbox\",\"kind\":\"ui\",\"props\":{\"checked\":false,\"color\":\"blue-500\",\"disabled\":false,\"labelRef\":\"\",\"name\":\"\",\"readOnly\":false,\"value\":\"\",\"field\":null,\"className\":\"\",\"children\":\"Is Default\",\"style\":{},\"sqlDefaultValue\":\"true\"},\"events\":{\"onChange\":\"\"},\"bindings\":{\"checked\":{\"sourceId\":\"cmp_msyfmegt_jkkn3\",\"path\":\"IsDefault\"}},\"children\":[],\"ref\":\"checkbox1\"},{\"id\":\"cmp_msyjebu2_27tqi\",\"type\":\"Checkbox\",\"kind\":\"ui\",\"props\":{\"checked\":false,\"color\":\"blue-500\",\"disabled\":false,\"labelRef\":\"\",\"name\":\"\",\"readOnly\":false,\"value\":\"\",\"field\":null,\"className\":\"\",\"children\":\"Is Static\",\"style\":{},\"sqlDefaultValue\":\"true\"},\"events\":{\"onChange\":\"\"},\"bindings\":{\"checked\":{\"sourceId\":\"cmp_msyfmegt_jkkn3\",\"path\":\"IsStatic\"}},\"children\":[],\"ref\":\"checkbox2\"},{\"id\":\"cmp_msyjmetn_gojol\",\"type\":\"Checkbox\",\"kind\":\"ui\",\"props\":{\"checked\":false,\"color\":\"blue-500\",\"disabled\":false,\"labelRef\":\"\",\"name\":\"\",\"readOnly\":false,\"value\":\"\",\"field\":null,\"className\":\"\",\"children\":\"Is Public\",\"style\":{},\"sqlDefaultValue\":\"false\"},\"events\":{\"onChange\":\"\"},\"bindings\":{\"checked\":{\"sourceId\":\"cmp_msyfmegt_jkkn3\",\"path\":\"IsPublic\"}},\"children\":[],\"ref\":\"checkbox3\"},{\"id\":\"cmp_msyjo0g8_oathk\",\"type\":\"Input\",\"kind\":\"ui\",\"props\":{\"asElement\":\"\",\"disabled\":false,\"invalid\":false,\"rows\":3,\"prefix\":\"Entity Version : \",\"size\":\"md\",\"suffix\":\"\",\"textArea\":false,\"type\":\"number\",\"unstyle\":false,\"field\":\"\",\"form\":\"\",\"className\":\"\",\"children\":\"\",\"style\":{},\"placeholder\":\"Metin girin...\",\"value\":\"\",\"name\":\"\",\"autoComplete\":\"off\",\"autoFocus\":false,\"readOnly\":false,\"required\":false,\"maxLength\":\"\",\"minLength\":\"\",\"pattern\":\"\",\"sqlDefaultValue\":\"3\"},\"events\":{\"onChange\":\"\"},\"bindings\":{\"value\":{\"sourceId\":\"cmp_msyfmegt_jkkn3\",\"path\":\"EntityVersion\"}},\"children\":[],\"ref\":\"input4\"},{\"id\":\"cmp_msyjplfq_py8lz\",\"type\":\"Input\",\"kind\":\"ui\",\"props\":{\"asElement\":\"\",\"disabled\":false,\"invalid\":false,\"rows\":3,\"prefix\":\"Extra Properties :\",\"size\":\"md\",\"suffix\":\"\",\"textArea\":false,\"type\":\"text\",\"unstyle\":false,\"field\":\"\",\"form\":\"\",\"className\":\"\",\"children\":\"\",\"style\":{},\"placeholder\":\"Metin girin...\",\"value\":\"\",\"name\":\"\",\"autoComplete\":\"off\",\"autoFocus\":false,\"readOnly\":false,\"required\":false,\"maxLength\":\"\",\"minLength\":\"\",\"pattern\":\"\",\"sqlDefaultValue\":\"{}\"},\"events\":{\"onChange\":\"\"},\"bindings\":{\"value\":{\"sourceId\":\"cmp_msyfmegt_jkkn3\",\"path\":\"ExtraProperties\"}},\"children\":[],\"ref\":\"input5\"}],\"ref\":\"form1\"},{\"id\":\"cmp_mt023r5u_rb2jv\",\"type\":\"Grid\",\"kind\":\"ui\",\"props\":{\"hoverable\":true,\"overflow\":true,\"borderlessRow\":false,\"compact\":false,\"cols\":\"3\",\"rows\":\"2\",\"gap\":\"4\",\"colGap\":\"4\",\"rowGap\":\"4\",\"responsive\":true,\"smCols\":\"1\",\"mdCols\":\"2\",\"lgCols\":\"3\",\"xlCols\":\"4\",\"autoFit\":false,\"minColWidth\":\"200px\",\"height\":\"auto\",\"className\":\"mt-4\",\"items\":[{\"id\":\"grid-item-1\",\"content\":\"Grid Item 1\",\"colSpan\":1,\"rowSpan\":1,\"colStart\":1,\"rowStart\":1,\"className\":\"bg-blue-100 p-4 border rounded\"},{\"id\":\"grid-item-2\",\"content\":\"Grid Item 2\",\"colSpan\":1,\"rowSpan\":1,\"colStart\":2,\"rowStart\":1,\"className\":\"bg-green-100 p-4 border rounded\"},{\"id\":\"grid-item-3\",\"content\":\"Grid Item 3\",\"colSpan\":1,\"rowSpan\":1,\"colStart\":3,\"rowStart\":1,\"className\":\"bg-yellow-100 p-4 border rounded\"},{\"id\":\"grid-item-4\",\"content\":\"Grid Item 4\",\"colSpan\":2,\"rowSpan\":1,\"colStart\":1,\"rowStart\":2,\"className\":\"bg-purple-100 p-4 border rounded\"},{\"id\":\"grid-item-5\",\"content\":\"Grid Item 5\",\"colSpan\":1,\"rowSpan\":1,\"colStart\":3,\"rowStart\":2,\"className\":\"bg-red-100 p-4 border rounded\"}],\"dataColumns\":[\"UserId\"],\"columnLookups\":{\"UserId\":{\"sourceId\":\"source_gq_0q1dz\",\"path\":\"\",\"valueField\":\"Id\",\"textField\":\"UserName\"}},\"columnCaptions\":{}},\"events\":{},\"bindings\":{\"items\":{\"sourceId\":\"source_tq_veb4m\",\"path\":\"\"}},\"children\":[],\"ref\":\"grid1\"},{\"id\":\"cmp_mt0288i3_ebpj5\",\"type\":\"ListView\",\"kind\":\"platform\",\"props\":{\"listFormCode\":\"AbpIdentity.Roles\",\"height\":\"520px\",\"filters\":[{\"id\":\"filter_y9_tr73w\",\"field\":\"Id\",\"operator\":\"eq\",\"source\":\"record\",\"value\":\"form1.Id\",\"previewValue\":\"\",\"required\":true}],\"className\":\"mt-4\"},\"events\":{},\"bindings\":{},\"children\":[],\"ref\":\"listView1\"}],\"canvas\":{\"width\":\"responsive\"},\"lifecycle\":{\"onMount\":\"\"},\"dataSources\":[{\"id\":\"source_5w_5ibpv\",\"name\":\"AbpRoles · GetList\",\"method\":\"GET\",\"url\":\"/api/app/crudendpoint/AbpRoles\",\"responsePath\":\"\"},{\"id\":\"source_i7_msox4\",\"name\":\"AbpRoles · GetById\",\"method\":\"GET\",\"url\":\"/api/app/crudendpoint/AbpRoles/{id}\",\"responsePath\":\"\"},{\"id\":\"source_0t_avok9\",\"name\":\"AbpRoles · Delete\",\"method\":\"DELETE\",\"url\":\"/api/app/crudendpoint/AbpRoles/{id}\",\"responsePath\":\"\"},{\"id\":\"source_h3_484qa\",\"name\":\"AbpRoles · Create\",\"method\":\"POST\",\"url\":\"/api/app/crudendpoint/AbpRoles\",\"responsePath\":\"\"},{\"id\":\"source_gb_d9cvs\",\"name\":\"AbpRoles · Update\",\"method\":\"PUT\",\"url\":\"/api/app/crudendpoint/AbpRoles/{id}\",\"responsePath\":\"\"},{\"id\":\"source_tq_veb4m\",\"name\":\"AbpUserRoles · GetList\",\"method\":\"GET\",\"url\":\"/api/app/crudendpoint/AbpUserRoles\",\"responsePath\":\"\",\"filters\":[{\"id\":\"filter_sw_86e0q\",\"field\":\"RoleId\",\"operator\":\"eq\",\"source\":\"record\",\"value\":\"form1.id\",\"previewValue\":\"\",\"required\":true}]},{\"id\":\"source_d2_bri5s\",\"name\":\"AbpUserRoles · GetById\",\"method\":\"GET\",\"url\":\"/api/app/crudendpoint/AbpUserRoles/{id}\",\"responsePath\":\"\"},{\"id\":\"source_gq_0q1dz\",\"name\":\"AbpUsers · GetList\",\"method\":\"GET\",\"url\":\"/api/app/crudendpoint/AbpUsers\",\"responsePath\":\"\"},{\"id\":\"source_u8_nu1lu\",\"name\":\"AbpUsers · GetById\",\"method\":\"GET\",\"url\":\"/api/app/crudendpoint/AbpUsers/{id}\",\"responsePath\":\"\"}],\"permissionCode\":\"\"}}", + "Id": "8389ccbc-3e7b-738d-35aa-3a237eea9b9d", + "Name": "DynamicEntityComponent", + "RoutePath": "/admin/dynamic-entity", + "Description": null, + "IsActive": true, + "Dependencies": "", + "Code": "import React, { useEffect, useState } from \"react\";\nimport axios from \"axios\";\n\ninterface DynamicEntityComponentProps {\n title: string;\n}\n\nconst api = axios.create({\n baseURL: \"https://localhost:44344/\",\n});\n\nconst DynamicEntityComponent: React.FC = ({ title }) => {\n const [data, setData] = useState>([]);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState(null);\n\n useEffect(() => {\n const fetchData = async () => {\n setLoading(true);\n setError(null);\n\n try {\n const res = await api.get(`/api/app/crudendpoint/${title}`);\n const raw = Array.isArray(res.data) ? res.data : res.data?.items ?? [];\n\n const filtered = raw.map((item: any) => ({\n id: item.Id ?? item.id,\n name: item.Name ?? item.name,\n }));\n\n setData(filtered);\n } catch (err: any) {\n setError(err.message || \"Failed to fetch data\");\n } finally {\n setLoading(false);\n }\n };\n\n if (title) fetchData();\n }, [title]);\n\n if (loading) return
Loading...
;\n if (error) return
Error: {error}
;\n if (!data.length) return
No records found
;\n\n const headers = [\"id\", \"name\", \"actions\"];\n\n return (\n
\n \n \n \n {headers.map((key) => (\n \n {key === \"actions\" ? \"Actions\" : key}\n \n ))}\n \n \n \n {data.map((item, rowIndex) => (\n \n \n \n \n \n ))}\n \n
\n {item.id}\n \n {item.name}\n \n alert(item.name)}\n shape=\"round\"\n size=\"xs\"\n variant=\"solid\"\n id=\"c_mrix4c1a_qrybhk\"\n >\n Show Name\n \n
\n
\n );\n};\n\nexport default DynamicEntityComponent;", + "Props": null, + "DataSources": "", + "LastModificationTime": null, + "CreationTime": "2026-09-05T17:22:11" + }, + { + "Id": "1acc9af1-6d5e-2940-160e-3a237eea9c0a", + "Name": "NewComponent", + "RoutePath": "/admin/new-component", "Description": "", "IsActive": true, "Dependencies": [], + "Code": "/*__SOZSOFT_VISUAL_DESIGNER__%7B%22version%22%3A1%2C%22sourceMode%22%3A%22visual%22%2C%22nodes%22%3A%5B%5D%2C%22canvas%22%3A%7B%22width%22%3A%22responsive%22%7D%2C%22lifecycle%22%3A%7B%22onMount%22%3A%22%22%7D%2C%22dataSources%22%3A%5B%5D%7D__*/\nconst NewComponent = () => {\n\n\n return (\n <>\n\n \n )\n}\n\nexport default NewComponent", + "Props": "{\"visualDesigner\":{\"version\":1,\"sourceMode\":\"visual\",\"nodes\":[],\"canvas\":{\"width\":\"responsive\"},\"lifecycle\":{\"onMount\":\"\"},\"dataSources\":[]}}", + "DataSources": [] + }, + { + "Id": "514a142f-8aaa-685f-fd13-3a237eea9c27", + "Name": "RoleListComponent", + "RoutePath": "/admin/roles-list", + "Description": null, + "IsActive": true, + "Dependencies": [ + "DynamicEntityComponent" + ], + "Code": "const RoleListComponent = ({\n title = \"AbpRoles\"\n}) => {\n return (\n \n );\n};\n\nexport default RoleListComponent;", + "Props": null, + "DataSources": [] + }, + { + "Id": "932daf62-9f6c-0c8c-08c0-3a237eea9c19", + "Name": "RoleComponent", + "RoutePath": "/admin/roles", + "Description": "", + "IsActive": true, + "Dependencies": [], + "Code": "/*__SOZSOFT_VISUAL_DESIGNER__%7B%22version%22%3A1%2C%22sourceMode%22%3A%22visual%22%2C%22nodes%22%3A%5B%7B%22id%22%3A%22cmp_msyfmegt_jkkn3%22%2C%22type%22%3A%22Form%22%2C%22kind%22%3A%22layout%22%2C%22props%22%3A%7B%22selectEndpoint%22%3A%22source_5w_5ibpv%22%2C%22insertEndpoint%22%3A%22source_h3_484qa%22%2C%22updateEndpoint%22%3A%22source_gb_d9cvs%22%2C%22deleteEndpoint%22%3A%22source_0t_avok9%22%2C%22keyFieldName%22%3A%22id%22%2C%22collectionPath%22%3A%22%22%2C%22keySource%22%3A%22query%22%2C%22keyParamName%22%3A%22id%22%2C%22previewKeyValue%22%3A%22%22%2C%22autoLoad%22%3Atrue%2C%22showToolbar%22%3Atrue%2C%22gap%22%3A16%2C%22className%22%3A%22%22%2C%22title%22%3A%22%3A%3AAbpIdentity.Roles%22%2C%22columnCount%22%3A2%2C%22selectPermission%22%3A%22%22%2C%22insertPermission%22%3A%22%22%7D%2C%22events%22%3A%7B%22onLoad%22%3A%22%22%2C%22onRecordChange%22%3A%22%22%2C%22onFieldChange%22%3A%22%22%2C%22onNewRecord%22%3A%22%22%2C%22onModeChange%22%3A%22%22%2C%22onBeforeSave%22%3A%22%22%2C%22onAfterSave%22%3A%22%22%2C%22onBeforeDelete%22%3A%22%22%2C%22onAfterDelete%22%3A%22%22%2C%22onError%22%3A%22%22%7D%2C%22bindings%22%3A%7B%7D%2C%22children%22%3A%5B%7B%22id%22%3A%22cmp_msyfpfia_ifhdq%22%2C%22type%22%3A%22Input%22%2C%22kind%22%3A%22ui%22%2C%22props%22%3A%7B%22asElement%22%3A%22%22%2C%22disabled%22%3Afalse%2C%22invalid%22%3Afalse%2C%22rows%22%3A3%2C%22prefix%22%3A%22%3A%3AApp.Platform.Name%22%2C%22size%22%3A%22md%22%2C%22suffix%22%3A%22%22%2C%22textArea%22%3Afalse%2C%22type%22%3A%22text%22%2C%22unstyle%22%3Afalse%2C%22field%22%3A%22%22%2C%22form%22%3A%22%22%2C%22className%22%3A%22%22%2C%22children%22%3A%22%22%2C%22style%22%3A%7B%7D%2C%22placeholder%22%3A%22Metin%20girin...%22%2C%22value%22%3A%22%22%2C%22name%22%3A%22%22%2C%22autoComplete%22%3A%22off%22%2C%22autoFocus%22%3Afalse%2C%22readOnly%22%3Afalse%2C%22required%22%3Afalse%2C%22maxLength%22%3A%22%22%2C%22minLength%22%3A%22%22%2C%22pattern%22%3A%22%22%2C%22sqlDefaultValue%22%3A%22%22%7D%2C%22events%22%3A%7B%22onChange%22%3A%22%22%7D%2C%22bindings%22%3A%7B%22value%22%3A%7B%22sourceId%22%3A%22cmp_msyfmegt_jkkn3%22%2C%22path%22%3A%22Name%22%7D%7D%2C%22children%22%3A%5B%5D%2C%22ref%22%3A%22input1%22%7D%2C%7B%22id%22%3A%22cmp_msyjl707_crcn7%22%2C%22type%22%3A%22Input%22%2C%22kind%22%3A%22ui%22%2C%22props%22%3A%7B%22asElement%22%3A%22%22%2C%22disabled%22%3Afalse%2C%22invalid%22%3Afalse%2C%22rows%22%3A3%2C%22prefix%22%3A%22Normalized%20Name%20%3A%22%2C%22size%22%3A%22md%22%2C%22suffix%22%3A%22%22%2C%22textArea%22%3Afalse%2C%22type%22%3A%22text%22%2C%22unstyle%22%3Afalse%2C%22field%22%3A%22%22%2C%22form%22%3A%22%22%2C%22className%22%3A%22%22%2C%22children%22%3A%22%22%2C%22style%22%3A%7B%7D%2C%22placeholder%22%3A%22Metin%20girin...%22%2C%22value%22%3A%22%22%2C%22name%22%3A%22%22%2C%22autoComplete%22%3A%22off%22%2C%22autoFocus%22%3Afalse%2C%22readOnly%22%3Afalse%2C%22required%22%3Afalse%2C%22maxLength%22%3A%22%22%2C%22minLength%22%3A%22%22%2C%22pattern%22%3A%22%22%7D%2C%22events%22%3A%7B%22onChange%22%3A%22%22%7D%2C%22bindings%22%3A%7B%22value%22%3A%7B%22sourceId%22%3A%22cmp_msyfmegt_jkkn3%22%2C%22path%22%3A%22NormalizedName%22%7D%7D%2C%22children%22%3A%5B%5D%2C%22ref%22%3A%22input2%22%7D%2C%7B%22id%22%3A%22cmp_msyjdodn_u3o74%22%2C%22type%22%3A%22Input%22%2C%22kind%22%3A%22ui%22%2C%22props%22%3A%7B%22asElement%22%3A%22%22%2C%22disabled%22%3Afalse%2C%22invalid%22%3Afalse%2C%22rows%22%3A3%2C%22prefix%22%3A%22Concurrent%20Stamp%20%3A%20%22%2C%22size%22%3A%22md%22%2C%22suffix%22%3A%22%22%2C%22textArea%22%3Afalse%2C%22type%22%3A%22text%22%2C%22unstyle%22%3Afalse%2C%22field%22%3A%22%22%2C%22form%22%3A%22%22%2C%22className%22%3A%22%22%2C%22children%22%3A%22%22%2C%22style%22%3A%7B%7D%2C%22placeholder%22%3A%22Metin%20girin...%22%2C%22value%22%3A%22%22%2C%22name%22%3A%22%22%2C%22autoComplete%22%3A%22off%22%2C%22autoFocus%22%3Afalse%2C%22readOnly%22%3Afalse%2C%22required%22%3Afalse%2C%22maxLength%22%3A%22%22%2C%22minLength%22%3A%22%22%2C%22pattern%22%3A%22%22%7D%2C%22events%22%3A%7B%22onChange%22%3A%22%22%7D%2C%22bindings%22%3A%7B%22value%22%3A%7B%22sourceId%22%3A%22cmp_msyfmegt_jkkn3%22%2C%22path%22%3A%22ConcurrencyStamp%22%7D%7D%2C%22children%22%3A%5B%5D%2C%22ref%22%3A%22input3%22%7D%2C%7B%22id%22%3A%22cmp_msyje9ys_2c08a%22%2C%22type%22%3A%22Checkbox%22%2C%22kind%22%3A%22ui%22%2C%22props%22%3A%7B%22checked%22%3Afalse%2C%22color%22%3A%22blue-500%22%2C%22disabled%22%3Afalse%2C%22labelRef%22%3A%22%22%2C%22name%22%3A%22%22%2C%22readOnly%22%3Afalse%2C%22value%22%3A%22%22%2C%22field%22%3Anull%2C%22className%22%3A%22%22%2C%22children%22%3A%22Is%20Default%22%2C%22style%22%3A%7B%7D%2C%22sqlDefaultValue%22%3A%22true%22%7D%2C%22events%22%3A%7B%22onChange%22%3A%22%22%7D%2C%22bindings%22%3A%7B%22checked%22%3A%7B%22sourceId%22%3A%22cmp_msyfmegt_jkkn3%22%2C%22path%22%3A%22IsDefault%22%7D%7D%2C%22children%22%3A%5B%5D%2C%22ref%22%3A%22checkbox1%22%7D%2C%7B%22id%22%3A%22cmp_msyjebu2_27tqi%22%2C%22type%22%3A%22Checkbox%22%2C%22kind%22%3A%22ui%22%2C%22props%22%3A%7B%22checked%22%3Afalse%2C%22color%22%3A%22blue-500%22%2C%22disabled%22%3Afalse%2C%22labelRef%22%3A%22%22%2C%22name%22%3A%22%22%2C%22readOnly%22%3Afalse%2C%22value%22%3A%22%22%2C%22field%22%3Anull%2C%22className%22%3A%22%22%2C%22children%22%3A%22Is%20Static%22%2C%22style%22%3A%7B%7D%2C%22sqlDefaultValue%22%3A%22true%22%7D%2C%22events%22%3A%7B%22onChange%22%3A%22%22%7D%2C%22bindings%22%3A%7B%22checked%22%3A%7B%22sourceId%22%3A%22cmp_msyfmegt_jkkn3%22%2C%22path%22%3A%22IsStatic%22%7D%7D%2C%22children%22%3A%5B%5D%2C%22ref%22%3A%22checkbox2%22%7D%2C%7B%22id%22%3A%22cmp_msyjmetn_gojol%22%2C%22type%22%3A%22Checkbox%22%2C%22kind%22%3A%22ui%22%2C%22props%22%3A%7B%22checked%22%3Afalse%2C%22color%22%3A%22blue-500%22%2C%22disabled%22%3Afalse%2C%22labelRef%22%3A%22%22%2C%22name%22%3A%22%22%2C%22readOnly%22%3Afalse%2C%22value%22%3A%22%22%2C%22field%22%3Anull%2C%22className%22%3A%22%22%2C%22children%22%3A%22Is%20Public%22%2C%22style%22%3A%7B%7D%2C%22sqlDefaultValue%22%3A%22false%22%7D%2C%22events%22%3A%7B%22onChange%22%3A%22%22%7D%2C%22bindings%22%3A%7B%22checked%22%3A%7B%22sourceId%22%3A%22cmp_msyfmegt_jkkn3%22%2C%22path%22%3A%22IsPublic%22%7D%7D%2C%22children%22%3A%5B%5D%2C%22ref%22%3A%22checkbox3%22%7D%2C%7B%22id%22%3A%22cmp_msyjo0g8_oathk%22%2C%22type%22%3A%22Input%22%2C%22kind%22%3A%22ui%22%2C%22props%22%3A%7B%22asElement%22%3A%22%22%2C%22disabled%22%3Afalse%2C%22invalid%22%3Afalse%2C%22rows%22%3A3%2C%22prefix%22%3A%22Entity%20Version%20%3A%20%22%2C%22size%22%3A%22md%22%2C%22suffix%22%3A%22%22%2C%22textArea%22%3Afalse%2C%22type%22%3A%22number%22%2C%22unstyle%22%3Afalse%2C%22field%22%3A%22%22%2C%22form%22%3A%22%22%2C%22className%22%3A%22%22%2C%22children%22%3A%22%22%2C%22style%22%3A%7B%7D%2C%22placeholder%22%3A%22Metin%20girin...%22%2C%22value%22%3A%22%22%2C%22name%22%3A%22%22%2C%22autoComplete%22%3A%22off%22%2C%22autoFocus%22%3Afalse%2C%22readOnly%22%3Afalse%2C%22required%22%3Afalse%2C%22maxLength%22%3A%22%22%2C%22minLength%22%3A%22%22%2C%22pattern%22%3A%22%22%2C%22sqlDefaultValue%22%3A%223%22%7D%2C%22events%22%3A%7B%22onChange%22%3A%22%22%7D%2C%22bindings%22%3A%7B%22value%22%3A%7B%22sourceId%22%3A%22cmp_msyfmegt_jkkn3%22%2C%22path%22%3A%22EntityVersion%22%7D%7D%2C%22children%22%3A%5B%5D%2C%22ref%22%3A%22input4%22%7D%2C%7B%22id%22%3A%22cmp_msyjplfq_py8lz%22%2C%22type%22%3A%22Input%22%2C%22kind%22%3A%22ui%22%2C%22props%22%3A%7B%22asElement%22%3A%22%22%2C%22disabled%22%3Afalse%2C%22invalid%22%3Afalse%2C%22rows%22%3A3%2C%22prefix%22%3A%22Extra%20Properties%20%3A%22%2C%22size%22%3A%22md%22%2C%22suffix%22%3A%22%22%2C%22textArea%22%3Afalse%2C%22type%22%3A%22text%22%2C%22unstyle%22%3Afalse%2C%22field%22%3A%22%22%2C%22form%22%3A%22%22%2C%22className%22%3A%22%22%2C%22children%22%3A%22%22%2C%22style%22%3A%7B%7D%2C%22placeholder%22%3A%22Metin%20girin...%22%2C%22value%22%3A%22%22%2C%22name%22%3A%22%22%2C%22autoComplete%22%3A%22off%22%2C%22autoFocus%22%3Afalse%2C%22readOnly%22%3Afalse%2C%22required%22%3Afalse%2C%22maxLength%22%3A%22%22%2C%22minLength%22%3A%22%22%2C%22pattern%22%3A%22%22%2C%22sqlDefaultValue%22%3A%22%7B%7D%22%7D%2C%22events%22%3A%7B%22onChange%22%3A%22%22%7D%2C%22bindings%22%3A%7B%22value%22%3A%7B%22sourceId%22%3A%22cmp_msyfmegt_jkkn3%22%2C%22path%22%3A%22ExtraProperties%22%7D%7D%2C%22children%22%3A%5B%5D%2C%22ref%22%3A%22input5%22%7D%5D%2C%22ref%22%3A%22form1%22%7D%2C%7B%22id%22%3A%22cmp_mt023r5u_rb2jv%22%2C%22type%22%3A%22Grid%22%2C%22kind%22%3A%22ui%22%2C%22props%22%3A%7B%22hoverable%22%3Atrue%2C%22overflow%22%3Atrue%2C%22borderlessRow%22%3Afalse%2C%22compact%22%3Afalse%2C%22cols%22%3A%223%22%2C%22rows%22%3A%222%22%2C%22gap%22%3A%224%22%2C%22colGap%22%3A%224%22%2C%22rowGap%22%3A%224%22%2C%22responsive%22%3Atrue%2C%22smCols%22%3A%221%22%2C%22mdCols%22%3A%222%22%2C%22lgCols%22%3A%223%22%2C%22xlCols%22%3A%224%22%2C%22autoFit%22%3Afalse%2C%22minColWidth%22%3A%22200px%22%2C%22height%22%3A%22auto%22%2C%22className%22%3A%22mt-4%22%2C%22items%22%3A%5B%7B%22id%22%3A%22grid-item-1%22%2C%22content%22%3A%22Grid%20Item%201%22%2C%22colSpan%22%3A1%2C%22rowSpan%22%3A1%2C%22colStart%22%3A1%2C%22rowStart%22%3A1%2C%22className%22%3A%22bg-blue-100%20p-4%20border%20rounded%22%7D%2C%7B%22id%22%3A%22grid-item-2%22%2C%22content%22%3A%22Grid%20Item%202%22%2C%22colSpan%22%3A1%2C%22rowSpan%22%3A1%2C%22colStart%22%3A2%2C%22rowStart%22%3A1%2C%22className%22%3A%22bg-green-100%20p-4%20border%20rounded%22%7D%2C%7B%22id%22%3A%22grid-item-3%22%2C%22content%22%3A%22Grid%20Item%203%22%2C%22colSpan%22%3A1%2C%22rowSpan%22%3A1%2C%22colStart%22%3A3%2C%22rowStart%22%3A1%2C%22className%22%3A%22bg-yellow-100%20p-4%20border%20rounded%22%7D%2C%7B%22id%22%3A%22grid-item-4%22%2C%22content%22%3A%22Grid%20Item%204%22%2C%22colSpan%22%3A2%2C%22rowSpan%22%3A1%2C%22colStart%22%3A1%2C%22rowStart%22%3A2%2C%22className%22%3A%22bg-purple-100%20p-4%20border%20rounded%22%7D%2C%7B%22id%22%3A%22grid-item-5%22%2C%22content%22%3A%22Grid%20Item%205%22%2C%22colSpan%22%3A1%2C%22rowSpan%22%3A1%2C%22colStart%22%3A3%2C%22rowStart%22%3A2%2C%22className%22%3A%22bg-red-100%20p-4%20border%20rounded%22%7D%5D%2C%22dataColumns%22%3A%5B%22UserId%22%5D%2C%22columnLookups%22%3A%7B%22UserId%22%3A%7B%22sourceId%22%3A%22source_gq_0q1dz%22%2C%22path%22%3A%22%22%2C%22valueField%22%3A%22Id%22%2C%22textField%22%3A%22UserName%22%7D%7D%2C%22columnCaptions%22%3A%7B%7D%7D%2C%22events%22%3A%7B%7D%2C%22bindings%22%3A%7B%22items%22%3A%7B%22sourceId%22%3A%22source_tq_veb4m%22%2C%22path%22%3A%22%22%7D%7D%2C%22children%22%3A%5B%5D%2C%22ref%22%3A%22grid1%22%7D%2C%7B%22id%22%3A%22cmp_mt0288i3_ebpj5%22%2C%22type%22%3A%22ListView%22%2C%22kind%22%3A%22platform%22%2C%22props%22%3A%7B%22listFormCode%22%3A%22AbpIdentity.Roles%22%2C%22height%22%3A%22520px%22%2C%22filters%22%3A%5B%7B%22id%22%3A%22filter_y9_tr73w%22%2C%22field%22%3A%22Id%22%2C%22operator%22%3A%22eq%22%2C%22source%22%3A%22record%22%2C%22value%22%3A%22form1.Id%22%2C%22previewValue%22%3A%22%22%2C%22required%22%3Atrue%7D%5D%2C%22className%22%3A%22mt-4%22%7D%2C%22events%22%3A%7B%7D%2C%22bindings%22%3A%7B%7D%2C%22children%22%3A%5B%5D%2C%22ref%22%3A%22listView1%22%7D%5D%2C%22canvas%22%3A%7B%22width%22%3A%22responsive%22%7D%2C%22lifecycle%22%3A%7B%22onMount%22%3A%22%22%7D%2C%22dataSources%22%3A%5B%7B%22id%22%3A%22source_5w_5ibpv%22%2C%22name%22%3A%22AbpRoles%20%C2%B7%20GetList%22%2C%22method%22%3A%22GET%22%2C%22url%22%3A%22%2Fapi%2Fapp%2Fcrudendpoint%2FAbpRoles%22%2C%22responsePath%22%3A%22%22%7D%2C%7B%22id%22%3A%22source_i7_msox4%22%2C%22name%22%3A%22AbpRoles%20%C2%B7%20GetById%22%2C%22method%22%3A%22GET%22%2C%22url%22%3A%22%2Fapi%2Fapp%2Fcrudendpoint%2FAbpRoles%2F%7Bid%7D%22%2C%22responsePath%22%3A%22%22%7D%2C%7B%22id%22%3A%22source_0t_avok9%22%2C%22name%22%3A%22AbpRoles%20%C2%B7%20Delete%22%2C%22method%22%3A%22DELETE%22%2C%22url%22%3A%22%2Fapi%2Fapp%2Fcrudendpoint%2FAbpRoles%2F%7Bid%7D%22%2C%22responsePath%22%3A%22%22%7D%2C%7B%22id%22%3A%22source_h3_484qa%22%2C%22name%22%3A%22AbpRoles%20%C2%B7%20Create%22%2C%22method%22%3A%22POST%22%2C%22url%22%3A%22%2Fapi%2Fapp%2Fcrudendpoint%2FAbpRoles%22%2C%22responsePath%22%3A%22%22%7D%2C%7B%22id%22%3A%22source_gb_d9cvs%22%2C%22name%22%3A%22AbpRoles%20%C2%B7%20Update%22%2C%22method%22%3A%22PUT%22%2C%22url%22%3A%22%2Fapi%2Fapp%2Fcrudendpoint%2FAbpRoles%2F%7Bid%7D%22%2C%22responsePath%22%3A%22%22%7D%2C%7B%22id%22%3A%22source_tq_veb4m%22%2C%22name%22%3A%22AbpUserRoles%20%C2%B7%20GetList%22%2C%22method%22%3A%22GET%22%2C%22url%22%3A%22%2Fapi%2Fapp%2Fcrudendpoint%2FAbpUserRoles%22%2C%22responsePath%22%3A%22%22%2C%22filters%22%3A%5B%7B%22id%22%3A%22filter_sw_86e0q%22%2C%22field%22%3A%22RoleId%22%2C%22operator%22%3A%22eq%22%2C%22source%22%3A%22record%22%2C%22value%22%3A%22form1.id%22%2C%22previewValue%22%3A%22%22%2C%22required%22%3Atrue%7D%5D%7D%2C%7B%22id%22%3A%22source_d2_bri5s%22%2C%22name%22%3A%22AbpUserRoles%20%C2%B7%20GetById%22%2C%22method%22%3A%22GET%22%2C%22url%22%3A%22%2Fapi%2Fapp%2Fcrudendpoint%2FAbpUserRoles%2F%7Bid%7D%22%2C%22responsePath%22%3A%22%22%7D%2C%7B%22id%22%3A%22source_gq_0q1dz%22%2C%22name%22%3A%22AbpUsers%20%C2%B7%20GetList%22%2C%22method%22%3A%22GET%22%2C%22url%22%3A%22%2Fapi%2Fapp%2Fcrudendpoint%2FAbpUsers%22%2C%22responsePath%22%3A%22%22%7D%2C%7B%22id%22%3A%22source_u8_nu1lu%22%2C%22name%22%3A%22AbpUsers%20%C2%B7%20GetById%22%2C%22method%22%3A%22GET%22%2C%22url%22%3A%22%2Fapi%2Fapp%2Fcrudendpoint%2FAbpUsers%2F%7Bid%7D%22%2C%22responsePath%22%3A%22%22%7D%5D%2C%22permissionCode%22%3A%22%22%7D__*/\nconst RoleComponent = () => {\n const getByPath = (value, path) => {\n if (!path) return value\n const readPath = (target, targetPath) => targetPath.split('.').filter(Boolean).reduce((current, key) => current?.[key], target)\n const directValue = readPath(value, path)\n if (directValue !== undefined) return directValue\n if (path.startsWith('result.')) return readPath(value, path.slice('result.'.length))\n return value?.result === undefined ? undefined : readPath(value.result, path)\n }\n\n const resolveLookupText = (lookup, value) => {\n if (!lookup || value === null || value === undefined || value === \"\") return undefined\n const rows = getByPath(lookup.rows, lookup.path)\n if (!Array.isArray(rows)) return undefined\n const key = String(value)\n const row = rows.find((item) => item && typeof item === \"object\" && String(getByPath(item, lookup.value) ?? \"\") === key)\n if (!row) return undefined\n const text = getByPath(row, lookup.text)\n return text === null || text === undefined || text === \"\" ? undefined : text\n }\n\n const localeCulture = () => {\n const injected = typeof getCulture === \"function\" ? getCulture() : \"\"\n if (injected) return injected\n if (typeof document !== \"undefined\" && document.documentElement.lang) return document.documentElement.lang\n return undefined\n }\n // Patterns come from the culture itself; a fixed day/month/year order would\n // print the same layout in every language.\n const localeDateOptions = { dateStyle: \"short\" }\n const localeTimeOptions = { dateStyle: \"short\", timeStyle: \"short\" }\n const localeIsoPattern = /^(\\d{4})-(\\d{2})-(\\d{2})(?:[T ](\\d{2}):(\\d{2})(?::(\\d{2})(?:\\.\\d+)?)?(Z|[+-]\\d{2}:?\\d{2})?)?$/\n const formatLocaleValue = (value) => {\n if (value === null || value === undefined) return \"\"\n const culture = localeCulture()\n if (typeof value === \"number\") {\n if (!Number.isFinite(value)) return String(value)\n // Grouping and the decimal separator are the culture's decision; only the\n // precision guard stays, so a long decimal is not rounded away.\n return new Intl.NumberFormat(culture, { maximumFractionDigits: 20 }).format(value)\n }\n if (value instanceof Date) {\n return Number.isNaN(value.getTime()) ? \"\" : new Intl.DateTimeFormat(culture, localeTimeOptions).format(value)\n }\n if (typeof value !== \"string\") return String(value)\n const parts = localeIsoPattern.exec(value.trim())\n if (!parts) return value\n const parsed = new Date(value)\n if (Number.isNaN(parsed.getTime())) return value\n if (parts[4] === undefined) {\n const day = new Date(Number(parts[1]), Number(parts[2]) - 1, Number(parts[3]))\n return new Intl.DateTimeFormat(culture, localeDateOptions).format(day)\n }\n return new Intl.DateTimeFormat(culture, localeTimeOptions).format(parsed)\n }\n\n const readSqlField = (record, field) => {\n if (!record || typeof record !== \"object\" || Array.isArray(record)) return undefined\n if (field in record) return record[field]\n const matched = Object.keys(record).find((key) => key.toLowerCase() === String(field).toLowerCase())\n return matched === undefined ? undefined : record[matched]\n }\n const toSqlRows = (value, collectionPath) => {\n const source = collectionPath ? getByPath(value, collectionPath) : value\n const rows = Array.isArray(source)\n ? source\n : source && typeof source === \"object\"\n ? (Array.isArray(source.items) ? source.items : [source])\n : []\n return rows.filter((item) => item && typeof item === \"object\" && !Array.isArray(item))\n }\n const setSqlField = (record, path, value) => {\n const keys = String(path || \"\").split(\".\").filter(Boolean)\n if (!keys.length) return record\n const next = { ...(record || {}) }\n let target = next\n for (let index = 0; index < keys.length - 1; index += 1) {\n const child = target[keys[index]]\n target[keys[index]] = child && typeof child === \"object\" && !Array.isArray(child) ? { ...child } : {}\n target = target[keys[index]]\n }\n target[keys[keys.length - 1]] = value\n return next\n }\n const bindSqlUrl = (url, record, keyField) => {\n let keyBound = false\n const boundUrl = String(url).replace(/\\{([^}]+)\\}|(?<=\\/):([A-Za-z_][A-Za-z0-9_]*)/g, (match, braced, colon) => {\n const name = (braced || colon || \"\").trim()\n if (!name) return match\n const value = readSqlField(record, name) ?? readSqlField(record, keyField)\n if (value === undefined || value === null || value === \"\") return match\n if (name.toLowerCase() === String(keyField).toLowerCase()) keyBound = true\n return encodeURIComponent(String(value))\n })\n return { url: boundUrl, keyBound }\n }\n const appendQueryParam = (url, name, value) =>\n url + (url.includes(\"?\") ? \"&\" : \"?\") + encodeURIComponent(name) + \"=\" + encodeURIComponent(value)\n const hasSqlUrlParams = (url) => /\\{[^}]+\\}|(?<=\\/):[A-Za-z_][A-Za-z0-9_]*/.test(String(url))\n const readUrlKey = (source, name) => {\n if (typeof window === \"undefined\") return \"\"\n if (source === \"query\") return new URLSearchParams(window.location.search).get(name) ?? \"\"\n const segments = window.location.pathname.split(\"/\").filter(Boolean)\n return segments.length ? decodeURIComponent(segments[segments.length - 1]) : \"\"\n }\n const filterSqlRowsByKey = (rows, keyField, keyValue) => {\n if (!keyValue) return rows\n const matched = rows.filter((row) => String(readSqlField(row, keyField) ?? \"\") === String(keyValue))\n // No match means the endpoint already filtered server side, or the key column\n // is named differently — keeping the rows beats showing an empty form.\n return matched.length ? matched : rows\n }\n // Update payload: only the columns the user actually edited, plus the key.\n const toSqlChanges = (original, current, keyField) => {\n const changes = {}\n const isSame = (left, right) => JSON.stringify(left ?? null) === JSON.stringify(right ?? null)\n Object.keys(current || {}).forEach((column) => {\n if (!isSame(original ? original[column] : undefined, current[column])) changes[column] = current[column]\n })\n const keyColumn = Object.keys(current || {}).find((column) => column.toLowerCase() === String(keyField).toLowerCase())\n if (keyColumn !== undefined && changes[keyColumn] === undefined) changes[keyColumn] = current[keyColumn]\n return changes\n }\n const callSqlEndpoint = (target, record, keyField, payload) => {\n // The URL is always bound from the full record: a placeholder may reference a\n // column that the update payload does not carry.\n const bound = bindSqlUrl(target.url, record, keyField)\n const request = { url: bound.url, method: target.method }\n if (target.method === \"POST\" || target.method === \"PUT\") request.data = payload === undefined ? record : payload\n const keyValue = readSqlField(record, keyField)\n const hasKey = keyValue !== undefined && keyValue !== null && keyValue !== \"\"\n // The key still has to reach the endpoint when the URL carries no placeholder.\n if (!bound.keyBound && hasKey && target.method !== \"POST\") request.params = { [keyField]: keyValue }\n return apiService.fetchData(request)\n }\n // A new record starts at the top of the form: the first enabled editor takes the\n // cursor, so a Yeni click is immediately followed by typing.\n const focusFirstSqlField = (host) => {\n if (typeof window === \"undefined\") return\n window.requestAnimationFrame(() => {\n const field = host.current?.querySelector(\"input:not([type='hidden']):not([disabled]):not([readonly]), textarea:not([disabled]):not([readonly]), select:not([disabled])\")\n if (!field) return\n field.focus()\n if (typeof field.select === \"function\") field.select()\n })\n }\n // Defaults declared on the bound children; only a new record starts from them.\n const buildSqlDefaults = (fields) => fields.reduce((record, field) => setSqlField(record, field.path, resolveSqlDefault(field)), {})\n const resolveSqlDefault = (field) => {\n if (field.token !== \"today\" && field.token !== \"now\") return field.value\n const now = new Date()\n const pad = (value) => String(value).padStart(2, \"0\")\n const day = now.getFullYear() + \"-\" + pad(now.getMonth() + 1) + \"-\" + pad(now.getDate())\n if (field.token === \"today\" || field.dateOnly) return day\n return day + \"T\" + pad(now.getHours()) + \":\" + pad(now.getMinutes()) + \":\" + pad(now.getSeconds())\n }\n const toSqlErrorMessage = (error) => error?.response?.data?.error?.message || error?.response?.data?.message || error?.message || translate(\"::App.Platform.IslemBasarisiz\")\n\n // Value of a filter that reads the page URL: a query string parameter, or a\n // path segment — the one after a segment named like the parameter, otherwise\n // the last one, which is the /orders/{id} detail page shape.\n const readDesignerUrlValue = (kind, name) => {\n if (typeof window === \"undefined\") return \"\"\n if (kind === \"query\") return new URLSearchParams(window.location.search).get(name) ?? \"\"\n const segments = window.location.pathname.split(\"/\").filter(Boolean)\n if (!segments.length) return \"\"\n const index = name ? segments.findIndex((segment) => segment.toLowerCase() === String(name).toLowerCase()) : -1\n if (index >= 0 && index < segments.length - 1) return decodeURIComponent(segments[index + 1])\n return decodeURIComponent(segments[segments.length - 1])\n }\n // Filters are appended as query parameters: `Column` for an equality and\n // `Column.operator` for anything else, which is what the endpoint parses.\n // Returns \"\" when a required filter has no value yet — the caller must skip the\n // request instead of loading the unfiltered collection.\n const buildFilteredUrl = (url, filters) => {\n const parts = []\n for (const filter of filters || []) {\n const value = filter.value === undefined || filter.value === null ? \"\" : String(filter.value)\n if (!value) {\n if (filter.required) return \"\"\n continue\n }\n parts.push(encodeURIComponent(filter.param) + \"=\" + encodeURIComponent(value))\n }\n if (!parts.length) return url\n return url + (url.includes(\"?\") ? \"&\" : \"?\") + parts.join(\"&\")\n }\n\n const buildPlatformFilterParams = (filters) => {\n const expressions = []\n for (const filter of filters || []) {\n if (filter.kind === \"isnull\" || filter.kind === \"notnull\") {\n expressions.push([filter.field, filter.operator, null])\n continue\n }\n const value = filter.value === undefined || filter.value === null ? \"\" : String(filter.value)\n if (!value) {\n if (filter.required) return null\n continue\n }\n if (filter.kind === \"in\") {\n const entries = value.split(\",\").map((entry) => entry.trim()).filter(Boolean).map((entry) => [filter.field, \"=\", entry])\n if (!entries.length) continue\n expressions.push(entries.reduce((result, entry) => (result ? [result, \"or\", entry] : entry), null))\n continue\n }\n expressions.push([filter.field, filter.operator, value])\n }\n const params = new URLSearchParams()\n const merged = expressions.reduce((result, expression) => (result ? [result, \"and\", expression] : expression), null)\n if (merged) params.set(\"filter\", JSON.stringify(merged))\n return params\n }\n\n const [data_source_5w_5ibpv, setData_source_5w_5ibpv] = React.useState(null)\n const [data_source_i7_msox4, setData_source_i7_msox4] = React.useState(null)\n const [data_source_0t_avok9] = React.useState(null)\n const [data_source_h3_484qa] = React.useState(null)\n const [data_source_gb_d9cvs] = React.useState(null)\n const [data_source_tq_veb4m, setData_source_tq_veb4m] = React.useState(null)\n const [data_source_d2_bri5s, setData_source_d2_bri5s] = React.useState(null)\n const [data_source_gq_0q1dz, setData_source_gq_0q1dz] = React.useState(null)\n const [data_source_u8_nu1lu, setData_source_u8_nu1lu] = React.useState(null)\n\n const sqlselect_cmp_msyfmegt_jkkn3 = { url: \"/api/app/crudendpoint/AbpRoles\", method: \"GET\", responsePath: \"\" }\n const sqlinsert_cmp_msyfmegt_jkkn3 = { url: \"/api/app/crudendpoint/AbpRoles\", method: \"POST\", responsePath: \"\" }\n const sqlupdate_cmp_msyfmegt_jkkn3 = { url: \"/api/app/crudendpoint/AbpRoles/{id}\", method: \"PUT\", responsePath: \"\" }\n const sqldelete_cmp_msyfmegt_jkkn3 = { url: \"/api/app/crudendpoint/AbpRoles/{id}\", method: \"DELETE\", responsePath: \"\" }\n const sqlCanSelect_cmp_msyfmegt_jkkn3 = true\n const sqlCanInsert_cmp_msyfmegt_jkkn3 = true\n const sqlCanUpdate_cmp_msyfmegt_jkkn3 = true\n const sqlCanDelete_cmp_msyfmegt_jkkn3 = true\n const sqlKey_cmp_msyfmegt_jkkn3 = \"id\"\n const sqlKeyParam_cmp_msyfmegt_jkkn3 = \"id\"\n const [data_cmp_msyfmegt_jkkn3, setData_cmp_msyfmegt_jkkn3] = React.useState({})\n const [sqlOriginal_cmp_msyfmegt_jkkn3, setSqlOriginal_cmp_msyfmegt_jkkn3] = React.useState({})\n const [sqlMode_cmp_msyfmegt_jkkn3, setSqlMode_cmp_msyfmegt_jkkn3] = React.useState(\"edit\")\n const [sqlIndex_cmp_msyfmegt_jkkn3, setSqlIndex_cmp_msyfmegt_jkkn3] = React.useState(0)\n const [sqlBusy_cmp_msyfmegt_jkkn3, setSqlBusy_cmp_msyfmegt_jkkn3] = React.useState(false)\n const [sqlError_cmp_msyfmegt_jkkn3, setSqlError_cmp_msyfmegt_jkkn3] = React.useState(\"\")\n // Container element, so New can hand the cursor to the first field of the form.\n const sqlHost_cmp_msyfmegt_jkkn3 = React.useRef(null)\n // The Select key can come from the page URL, which is how a detail page reads\n // /api/app/orders/{id} or /api/app/orders?id=… for a single record.\n const sqlUrlKey_cmp_msyfmegt_jkkn3 = readUrlKey(\"query\", sqlKeyParam_cmp_msyfmegt_jkkn3)\n const sqlSelectUrl_cmp_msyfmegt_jkkn3 = React.useMemo(() => {\n if (!sqlselect_cmp_msyfmegt_jkkn3) return \"\"\n const base = sqlUrlKey_cmp_msyfmegt_jkkn3\n ? (() => {\n const bound = bindSqlUrl(sqlselect_cmp_msyfmegt_jkkn3.url, { [sqlKeyParam_cmp_msyfmegt_jkkn3]: sqlUrlKey_cmp_msyfmegt_jkkn3 }, sqlKeyParam_cmp_msyfmegt_jkkn3)\n return bound.keyBound ? bound.url : appendQueryParam(bound.url, sqlKeyParam_cmp_msyfmegt_jkkn3, sqlUrlKey_cmp_msyfmegt_jkkn3)\n })()\n : sqlselect_cmp_msyfmegt_jkkn3.url\n return base\n }, [sqlUrlKey_cmp_msyfmegt_jkkn3])\n // A key in the page URL always narrows the result: a list endpoint ignores the\n // parameter server side, so the requested record is picked out here. Without a\n // key every row is kept and the navigation below takes over.\n const sqlRows_cmp_msyfmegt_jkkn3 = React.useMemo(\n () => filterSqlRowsByKey(toSqlRows(data_source_5w_5ibpv, \"\"), sqlKey_cmp_msyfmegt_jkkn3, sqlUrlKey_cmp_msyfmegt_jkkn3),\n [data_source_5w_5ibpv, sqlUrlKey_cmp_msyfmegt_jkkn3],\n )\n // A fresh result set resets the position; New mode is left untouched so an\n // unsaved draft is not overwritten by a re-render.\n React.useEffect(() => {\n setSqlIndex_cmp_msyfmegt_jkkn3((current) => (current < sqlRows_cmp_msyfmegt_jkkn3.length ? current : 0))\n }, [sqlRows_cmp_msyfmegt_jkkn3])\n React.useEffect(() => {\n const row = sqlRows_cmp_msyfmegt_jkkn3[sqlIndex_cmp_msyfmegt_jkkn3]\n if (!row) return\n setData_cmp_msyfmegt_jkkn3(row)\n setSqlOriginal_cmp_msyfmegt_jkkn3(row)\n setSqlMode_cmp_msyfmegt_jkkn3(\"edit\")\n }, [sqlRows_cmp_msyfmegt_jkkn3, sqlIndex_cmp_msyfmegt_jkkn3])\n const sqlHasKey_cmp_msyfmegt_jkkn3 = (() => {\n const value = readSqlField(data_cmp_msyfmegt_jkkn3, sqlKey_cmp_msyfmegt_jkkn3)\n return value !== undefined && value !== null && value !== \"\"\n })()\n const sqlSetField_cmp_msyfmegt_jkkn3 = React.useCallback((path, value) => setData_cmp_msyfmegt_jkkn3((current) => setSqlField(current, path, value)), [])\n const sqlReload_cmp_msyfmegt_jkkn3 = React.useCallback(async () => {\n // Without the Read permission the container is not rendered at all; the\n // request is dropped here so it is not fired for a form nobody can see.\n if (!sqlCanSelect_cmp_msyfmegt_jkkn3) return\n if (!sqlSelectUrl_cmp_msyfmegt_jkkn3) return\n // Calling a URL that still holds a placeholder is a guaranteed 400, so the\n // missing key is reported in the component instead.\n if (hasSqlUrlParams(sqlSelectUrl_cmp_msyfmegt_jkkn3)) {\n throw new Error(translate(\"::App.Form.MissingKeyParam\", { 0: sqlKeyParam_cmp_msyfmegt_jkkn3 }))\n }\n const response = await apiService.fetchData({ url: sqlSelectUrl_cmp_msyfmegt_jkkn3, method: \"GET\" })\n setData_source_5w_5ibpv(getByPath(response.data, sqlselect_cmp_msyfmegt_jkkn3.responsePath))\n }, [sqlSelectUrl_cmp_msyfmegt_jkkn3])\n // Reload wrapped with the busy/error handling the toolbar and mount effect need,\n // so a failing Select is reported in the component instead of the console.\n const sqlRefresh_cmp_msyfmegt_jkkn3 = React.useCallback(async () => {\n setSqlBusy_cmp_msyfmegt_jkkn3(true)\n setSqlError_cmp_msyfmegt_jkkn3(\"\")\n try {\n await sqlReload_cmp_msyfmegt_jkkn3()\n } catch (error) {\n setSqlError_cmp_msyfmegt_jkkn3(toSqlErrorMessage(error))\n } finally {\n setSqlBusy_cmp_msyfmegt_jkkn3(false)\n }\n }, [sqlReload_cmp_msyfmegt_jkkn3])\n const sqlNew_cmp_msyfmegt_jkkn3 = React.useCallback(() => {\n if (!sqlCanInsert_cmp_msyfmegt_jkkn3) return\n setSqlError_cmp_msyfmegt_jkkn3(\"\")\n // Defaults are resolved on every New, so an @today column carries the day the\n // record was opened rather than the day the component was generated.\n const sqlDraft_cmp_msyfmegt_jkkn3 = buildSqlDefaults([{\"path\":\"IsDefault\",\"value\":true},{\"path\":\"IsStatic\",\"value\":true},{\"path\":\"IsPublic\",\"value\":false},{\"path\":\"EntityVersion\",\"value\":3},{\"path\":\"ExtraProperties\",\"value\":\"{}\"}])\n setData_cmp_msyfmegt_jkkn3(sqlDraft_cmp_msyfmegt_jkkn3)\n setSqlOriginal_cmp_msyfmegt_jkkn3({})\n setSqlMode_cmp_msyfmegt_jkkn3(\"new\")\n // After the re-render that paints the empty form, not before it.\n focusFirstSqlField(sqlHost_cmp_msyfmegt_jkkn3)\n }, [])\n const sqlPrev_cmp_msyfmegt_jkkn3 = React.useCallback(() => setSqlIndex_cmp_msyfmegt_jkkn3((current) => Math.max(0, current - 1)), [])\n const sqlNext_cmp_msyfmegt_jkkn3 = React.useCallback(() => setSqlIndex_cmp_msyfmegt_jkkn3((current) => Math.min(sqlRows_cmp_msyfmegt_jkkn3.length - 1, current + 1)), [sqlRows_cmp_msyfmegt_jkkn3])\n const sqlSave_cmp_msyfmegt_jkkn3 = async () => {\n const isNew = sqlMode_cmp_msyfmegt_jkkn3 === \"new\"\n const target = isNew ? sqlinsert_cmp_msyfmegt_jkkn3 : sqlupdate_cmp_msyfmegt_jkkn3\n // The button is already hidden without the permission; a script calling save\n // through a ref must not be able to walk around it either.\n if (!(isNew ? sqlCanInsert_cmp_msyfmegt_jkkn3 : sqlCanUpdate_cmp_msyfmegt_jkkn3)) return\n if (!target) {\n setSqlError_cmp_msyfmegt_jkkn3(isNew ? translate(\"::App.Form.InsertEndpointMissing\") : translate(\"::App.Form.UpdateEndpointMissing\"))\n return\n }\n // Update carries only the edited columns plus the key; Insert sends the record.\n const payload = isNew ? data_cmp_msyfmegt_jkkn3 : toSqlChanges(sqlOriginal_cmp_msyfmegt_jkkn3, data_cmp_msyfmegt_jkkn3, sqlKey_cmp_msyfmegt_jkkn3)\n if (!isNew && Object.keys(payload).filter((column) => column.toLowerCase() !== String(sqlKey_cmp_msyfmegt_jkkn3).toLowerCase()).length === 0) {\n setSqlError_cmp_msyfmegt_jkkn3(translate(\"::App.Form.NoChanges\"))\n return\n }\n setSqlBusy_cmp_msyfmegt_jkkn3(true)\n setSqlError_cmp_msyfmegt_jkkn3(\"\")\n try {\n const response = await callSqlEndpoint(target, data_cmp_msyfmegt_jkkn3, sqlKey_cmp_msyfmegt_jkkn3, payload)\n // With a Select endpoint the reload is the source of truth and its effect\n // repopulates the form; without one, the response row is all there is.\n if (sqlselect_cmp_msyfmegt_jkkn3) {\n await sqlReload_cmp_msyfmegt_jkkn3()\n } else {\n const saved = toSqlRows(response?.data, \"\")[0]\n if (saved) {\n setData_cmp_msyfmegt_jkkn3(saved)\n setSqlOriginal_cmp_msyfmegt_jkkn3(saved)\n }\n }\n setSqlMode_cmp_msyfmegt_jkkn3(\"edit\")\n } catch (error) {\n setSqlError_cmp_msyfmegt_jkkn3(toSqlErrorMessage(error))\n } finally {\n setSqlBusy_cmp_msyfmegt_jkkn3(false)\n }\n }\n const sqlDelete_cmp_msyfmegt_jkkn3 = async () => {\n if (!sqlCanDelete_cmp_msyfmegt_jkkn3 || !sqldelete_cmp_msyfmegt_jkkn3 || !sqlHasKey_cmp_msyfmegt_jkkn3 || sqlMode_cmp_msyfmegt_jkkn3 === \"new\") return\n // Deleting cannot be undone from the form, so it always asks first.\n const confirmMessage = translate(\"::App.Form.DeleteConfirm\", { 0: sqlKey_cmp_msyfmegt_jkkn3 + \" = \" + readSqlField(data_cmp_msyfmegt_jkkn3, sqlKey_cmp_msyfmegt_jkkn3) })\n if (typeof window !== \"undefined\" && !window.confirm(confirmMessage)) return\n setSqlBusy_cmp_msyfmegt_jkkn3(true)\n setSqlError_cmp_msyfmegt_jkkn3(\"\")\n try {\n await callSqlEndpoint(sqldelete_cmp_msyfmegt_jkkn3, data_cmp_msyfmegt_jkkn3, sqlKey_cmp_msyfmegt_jkkn3)\n setData_cmp_msyfmegt_jkkn3({})\n setSqlOriginal_cmp_msyfmegt_jkkn3({})\n setSqlMode_cmp_msyfmegt_jkkn3(\"new\")\n await sqlReload_cmp_msyfmegt_jkkn3()\n } catch (error) {\n setSqlError_cmp_msyfmegt_jkkn3(toSqlErrorMessage(error))\n } finally {\n setSqlBusy_cmp_msyfmegt_jkkn3(false)\n }\n }\n React.useEffect(() => { void sqlRefresh_cmp_msyfmegt_jkkn3() }, [sqlRefresh_cmp_msyfmegt_jkkn3])\n\n const filters_source_tq_veb4m = [{ param: \"RoleId\", value: readSqlField(data_cmp_msyfmegt_jkkn3, \"id\"), required: true }]\n const filterKey_source_tq_veb4m = JSON.stringify(filters_source_tq_veb4m)\n React.useEffect(() => {\n const url = buildFilteredUrl(\"/api/app/crudendpoint/AbpUserRoles\", filters_source_tq_veb4m)\n // An unmet required filter leaves the component empty on purpose.\n if (!url) {\n setData_source_tq_veb4m(null)\n return\n }\n let active = true\n apiService.fetchData({ url, method: 'GET' }).then((response) => {\n if (active) setData_source_tq_veb4m(getByPath(response.data, \"\"))\n }).catch((error) => console.error(\"AbpUserRoles · GetList endpoint error:\", error))\n return () => { active = false }\n }, [filterKey_source_tq_veb4m])\n\n React.useEffect(() => {\n let active = true\n apiService.fetchData({ url: \"/api/app/crudendpoint/AbpUsers\", method: 'GET' }).then((response) => {\n if (active) setData_source_gq_0q1dz(getByPath(response.data, \"\"))\n }).catch((error) => console.error(\"AbpUsers · GetList endpoint error:\", error))\n return () => { active = false }\n }, [])\n\n const platformFilters_cmp_mt0288i3_ebpj5 = [{ field: \"Id\", operator: \"=\", kind: \"eq\", value: readSqlField(data_cmp_msyfmegt_jkkn3, \"Id\"), required: true }]\n const platformKey_cmp_mt0288i3_ebpj5 = JSON.stringify(platformFilters_cmp_mt0288i3_ebpj5)\n // The views keep the params object in their own state and write back to it, so\n // it must stay the same object until a filter value actually changes.\n const platformParams_cmp_mt0288i3_ebpj5 = React.useMemo(() => buildPlatformFilterParams(platformFilters_cmp_mt0288i3_ebpj5), [platformKey_cmp_mt0288i3_ebpj5])\n\n const handle_cmp_msyfpfia_ifhdq_onChange = (valueOrEvent, originalEvent) => {\n sqlSetField_cmp_msyfmegt_jkkn3(\"Name\", valueOrEvent?.target?.value ?? valueOrEvent)\n const event = valueOrEvent\n }\n\n const handle_cmp_msyjl707_crcn7_onChange = (valueOrEvent, originalEvent) => {\n sqlSetField_cmp_msyfmegt_jkkn3(\"NormalizedName\", valueOrEvent?.target?.value ?? valueOrEvent)\n const event = valueOrEvent\n }\n\n const handle_cmp_msyjdodn_u3o74_onChange = (valueOrEvent, originalEvent) => {\n sqlSetField_cmp_msyfmegt_jkkn3(\"ConcurrencyStamp\", valueOrEvent?.target?.value ?? valueOrEvent)\n const event = valueOrEvent\n }\n\n const handle_cmp_msyje9ys_2c08a_onChange = (valueOrEvent, originalEvent) => {\n sqlSetField_cmp_msyfmegt_jkkn3(\"IsDefault\", typeof originalEvent === \"object\" && originalEvent?.target ? Boolean(originalEvent.target.checked) : typeof valueOrEvent === \"object\" && valueOrEvent?.target ? Boolean(valueOrEvent.target.checked) : Boolean(valueOrEvent))\n const event = { checked: Boolean(valueOrEvent), originalEvent, target: originalEvent?.target }\n }\n\n const handle_cmp_msyjebu2_27tqi_onChange = (valueOrEvent, originalEvent) => {\n sqlSetField_cmp_msyfmegt_jkkn3(\"IsStatic\", typeof originalEvent === \"object\" && originalEvent?.target ? Boolean(originalEvent.target.checked) : typeof valueOrEvent === \"object\" && valueOrEvent?.target ? Boolean(valueOrEvent.target.checked) : Boolean(valueOrEvent))\n const event = { checked: Boolean(valueOrEvent), originalEvent, target: originalEvent?.target }\n }\n\n const handle_cmp_msyjmetn_gojol_onChange = (valueOrEvent, originalEvent) => {\n sqlSetField_cmp_msyfmegt_jkkn3(\"IsPublic\", typeof originalEvent === \"object\" && originalEvent?.target ? Boolean(originalEvent.target.checked) : typeof valueOrEvent === \"object\" && valueOrEvent?.target ? Boolean(valueOrEvent.target.checked) : Boolean(valueOrEvent))\n const event = { checked: Boolean(valueOrEvent), originalEvent, target: originalEvent?.target }\n }\n\n const handle_cmp_msyjo0g8_oathk_onChange = (valueOrEvent, originalEvent) => {\n sqlSetField_cmp_msyfmegt_jkkn3(\"EntityVersion\", valueOrEvent?.target?.value ?? valueOrEvent)\n const event = valueOrEvent\n }\n\n const handle_cmp_msyjplfq_py8lz_onChange = (valueOrEvent, originalEvent) => {\n sqlSetField_cmp_msyfmegt_jkkn3(\"ExtraProperties\", valueOrEvent?.target?.value ?? valueOrEvent)\n const event = valueOrEvent\n }\n\n return (\n <>\n {sqlCanSelect_cmp_msyfmegt_jkkn3 ? (\n {sqlHasKey_cmp_msyfmegt_jkkn3 ? sqlKey_cmp_msyfmegt_jkkn3 + \": \" + readSqlField(data_cmp_msyfmegt_jkkn3, sqlKey_cmp_msyfmegt_jkkn3) : sqlMode_cmp_msyfmegt_jkkn3 === \"new\" ? translate(\"::App.Platform.NewRecord\") : \"\"}}>\n
\n
\n \n \n \n \n {\"Is Default\"}\n \n \n {\"Is Static\"}\n \n \n {\"Is Public\"}\n \n \n \n
\n
\n {sqlRows_cmp_msyfmegt_jkkn3.length > 1 ? (\n <>\n \n {`${sqlIndex_cmp_msyfmegt_jkkn3 + 1} / ${sqlRows_cmp_msyfmegt_jkkn3.length}`}\n \n \n \n ) : null}\n {sqlinsert_cmp_msyfmegt_jkkn3 && sqlCanInsert_cmp_msyfmegt_jkkn3 ? : null}\n {(sqlMode_cmp_msyfmegt_jkkn3 === \"new\" ? sqlinsert_cmp_msyfmegt_jkkn3 && sqlCanInsert_cmp_msyfmegt_jkkn3 : sqlupdate_cmp_msyfmegt_jkkn3 && sqlCanUpdate_cmp_msyfmegt_jkkn3) ? : null}\n {sqldelete_cmp_msyfmegt_jkkn3 && sqlCanDelete_cmp_msyfmegt_jkkn3 ? : null}\n {sqlselect_cmp_msyfmegt_jkkn3 && sqlCanSelect_cmp_msyfmegt_jkkn3 ? : null}\n {sqlMode_cmp_msyfmegt_jkkn3 === \"new\" ? translate(\"::App.Platform.NewRecord\") : translate(\"::App.Platform.Editing\")}\n
\n {sqlError_cmp_msyfmegt_jkkn3 ?
{sqlError_cmp_msyfmegt_jkkn3}
: null}\n
\n
\n ) : null}\n
\n {(() => {\n const gridItems_cmp_mt023r5u_rb2jv = (Array.isArray(getByPath(data_source_tq_veb4m, \"\")) ? getByPath(data_source_tq_veb4m, \"\") : Array.isArray(data_source_tq_veb4m) ? data_source_tq_veb4m : null)\n // Nothing loaded yet — an endpoint still in flight, or a required filter\n // holding the request back — is an empty grid, not a binding error.\n if (gridItems_cmp_mt023r5u_rb2jv === null || gridItems_cmp_mt023r5u_rb2jv === undefined) return null\n if (!Array.isArray(gridItems_cmp_mt023r5u_rb2jv)) return
Grid items bağlantısı bir koleksiyon döndürmelidir.
\n const gridColumns_cmp_mt023r5u_rb2jv = [\"UserId\"]\n const gridLookups_cmp_mt023r5u_rb2jv = { \"UserId\": { rows: data_source_gq_0q1dz, path: \"\", value: \"Id\", text: \"UserName\" } }\n if (!gridColumns_cmp_mt023r5u_rb2jv.length) return
Preview için en az bir sütun seçin.
\n return (\n
\n \n \n {gridColumns_cmp_mt023r5u_rb2jv.map((column) => )}\n \n \n {gridItems_cmp_mt023r5u_rb2jv.map((gridItem_cmp_mt023r5u_rb2jv, rowIndex) => (\n \n {gridColumns_cmp_mt023r5u_rb2jv.map((column) => {\n const cellValue_cmp_mt023r5u_rb2jv = column === \"value\" ? gridItem_cmp_mt023r5u_rb2jv : getByPath(gridItem_cmp_mt023r5u_rb2jv, column)\n const lookedUp = resolveLookupText(gridLookups_cmp_mt023r5u_rb2jv[column], cellValue_cmp_mt023r5u_rb2jv)\n const cellText = lookedUp === undefined ? cellValue_cmp_mt023r5u_rb2jv : lookedUp\n const text = typeof cellText === \"object\" && cellText !== null && !(cellText instanceof Date) ? JSON.stringify(cellText) : formatLocaleValue(cellText) || \"—\"\n return \n })}\n \n ))}\n \n
{(column === \"value\" ? \"Value\" : column)}
{text}
\n
\n )\n })()}\n
\n {platformParams_cmp_mt0288i3_ebpj5 === null ? null : (\n \n )}\n \n )\n}\n\nexport default RoleComponent", + "Props": "{\"visualDesigner\":{\"version\":1,\"sourceMode\":\"visual\",\"nodes\":[{\"id\":\"cmp_msyfmegt_jkkn3\",\"type\":\"Form\",\"kind\":\"layout\",\"props\":{\"selectEndpoint\":\"source_5w_5ibpv\",\"insertEndpoint\":\"source_h3_484qa\",\"updateEndpoint\":\"source_gb_d9cvs\",\"deleteEndpoint\":\"source_0t_avok9\",\"keyFieldName\":\"id\",\"collectionPath\":\"\",\"keySource\":\"query\",\"keyParamName\":\"id\",\"previewKeyValue\":\"\",\"autoLoad\":true,\"showToolbar\":true,\"gap\":16,\"className\":\"\",\"title\":\"::AbpIdentity.Roles\",\"columnCount\":2,\"selectPermission\":\"\",\"insertPermission\":\"\"},\"events\":{\"onLoad\":\"\",\"onRecordChange\":\"\",\"onFieldChange\":\"\",\"onNewRecord\":\"\",\"onModeChange\":\"\",\"onBeforeSave\":\"\",\"onAfterSave\":\"\",\"onBeforeDelete\":\"\",\"onAfterDelete\":\"\",\"onError\":\"\"},\"bindings\":{},\"children\":[{\"id\":\"cmp_msyfpfia_ifhdq\",\"type\":\"Input\",\"kind\":\"ui\",\"props\":{\"asElement\":\"\",\"disabled\":false,\"invalid\":false,\"rows\":3,\"prefix\":\"::App.Platform.Name\",\"size\":\"md\",\"suffix\":\"\",\"textArea\":false,\"type\":\"text\",\"unstyle\":false,\"field\":\"\",\"form\":\"\",\"className\":\"\",\"children\":\"\",\"style\":{},\"placeholder\":\"Metin girin...\",\"value\":\"\",\"name\":\"\",\"autoComplete\":\"off\",\"autoFocus\":false,\"readOnly\":false,\"required\":false,\"maxLength\":\"\",\"minLength\":\"\",\"pattern\":\"\",\"sqlDefaultValue\":\"\"},\"events\":{\"onChange\":\"\"},\"bindings\":{\"value\":{\"sourceId\":\"cmp_msyfmegt_jkkn3\",\"path\":\"Name\"}},\"children\":[],\"ref\":\"input1\"},{\"id\":\"cmp_msyjl707_crcn7\",\"type\":\"Input\",\"kind\":\"ui\",\"props\":{\"asElement\":\"\",\"disabled\":false,\"invalid\":false,\"rows\":3,\"prefix\":\"Normalized Name :\",\"size\":\"md\",\"suffix\":\"\",\"textArea\":false,\"type\":\"text\",\"unstyle\":false,\"field\":\"\",\"form\":\"\",\"className\":\"\",\"children\":\"\",\"style\":{},\"placeholder\":\"Metin girin...\",\"value\":\"\",\"name\":\"\",\"autoComplete\":\"off\",\"autoFocus\":false,\"readOnly\":false,\"required\":false,\"maxLength\":\"\",\"minLength\":\"\",\"pattern\":\"\"},\"events\":{\"onChange\":\"\"},\"bindings\":{\"value\":{\"sourceId\":\"cmp_msyfmegt_jkkn3\",\"path\":\"NormalizedName\"}},\"children\":[],\"ref\":\"input2\"},{\"id\":\"cmp_msyjdodn_u3o74\",\"type\":\"Input\",\"kind\":\"ui\",\"props\":{\"asElement\":\"\",\"disabled\":false,\"invalid\":false,\"rows\":3,\"prefix\":\"Concurrent Stamp : \",\"size\":\"md\",\"suffix\":\"\",\"textArea\":false,\"type\":\"text\",\"unstyle\":false,\"field\":\"\",\"form\":\"\",\"className\":\"\",\"children\":\"\",\"style\":{},\"placeholder\":\"Metin girin...\",\"value\":\"\",\"name\":\"\",\"autoComplete\":\"off\",\"autoFocus\":false,\"readOnly\":false,\"required\":false,\"maxLength\":\"\",\"minLength\":\"\",\"pattern\":\"\"},\"events\":{\"onChange\":\"\"},\"bindings\":{\"value\":{\"sourceId\":\"cmp_msyfmegt_jkkn3\",\"path\":\"ConcurrencyStamp\"}},\"children\":[],\"ref\":\"input3\"},{\"id\":\"cmp_msyje9ys_2c08a\",\"type\":\"Checkbox\",\"kind\":\"ui\",\"props\":{\"checked\":false,\"color\":\"blue-500\",\"disabled\":false,\"labelRef\":\"\",\"name\":\"\",\"readOnly\":false,\"value\":\"\",\"field\":null,\"className\":\"\",\"children\":\"Is Default\",\"style\":{},\"sqlDefaultValue\":\"true\"},\"events\":{\"onChange\":\"\"},\"bindings\":{\"checked\":{\"sourceId\":\"cmp_msyfmegt_jkkn3\",\"path\":\"IsDefault\"}},\"children\":[],\"ref\":\"checkbox1\"},{\"id\":\"cmp_msyjebu2_27tqi\",\"type\":\"Checkbox\",\"kind\":\"ui\",\"props\":{\"checked\":false,\"color\":\"blue-500\",\"disabled\":false,\"labelRef\":\"\",\"name\":\"\",\"readOnly\":false,\"value\":\"\",\"field\":null,\"className\":\"\",\"children\":\"Is Static\",\"style\":{},\"sqlDefaultValue\":\"true\"},\"events\":{\"onChange\":\"\"},\"bindings\":{\"checked\":{\"sourceId\":\"cmp_msyfmegt_jkkn3\",\"path\":\"IsStatic\"}},\"children\":[],\"ref\":\"checkbox2\"},{\"id\":\"cmp_msyjmetn_gojol\",\"type\":\"Checkbox\",\"kind\":\"ui\",\"props\":{\"checked\":false,\"color\":\"blue-500\",\"disabled\":false,\"labelRef\":\"\",\"name\":\"\",\"readOnly\":false,\"value\":\"\",\"field\":null,\"className\":\"\",\"children\":\"Is Public\",\"style\":{},\"sqlDefaultValue\":\"false\"},\"events\":{\"onChange\":\"\"},\"bindings\":{\"checked\":{\"sourceId\":\"cmp_msyfmegt_jkkn3\",\"path\":\"IsPublic\"}},\"children\":[],\"ref\":\"checkbox3\"},{\"id\":\"cmp_msyjo0g8_oathk\",\"type\":\"Input\",\"kind\":\"ui\",\"props\":{\"asElement\":\"\",\"disabled\":false,\"invalid\":false,\"rows\":3,\"prefix\":\"Entity Version : \",\"size\":\"md\",\"suffix\":\"\",\"textArea\":false,\"type\":\"number\",\"unstyle\":false,\"field\":\"\",\"form\":\"\",\"className\":\"\",\"children\":\"\",\"style\":{},\"placeholder\":\"Metin girin...\",\"value\":\"\",\"name\":\"\",\"autoComplete\":\"off\",\"autoFocus\":false,\"readOnly\":false,\"required\":false,\"maxLength\":\"\",\"minLength\":\"\",\"pattern\":\"\",\"sqlDefaultValue\":\"3\"},\"events\":{\"onChange\":\"\"},\"bindings\":{\"value\":{\"sourceId\":\"cmp_msyfmegt_jkkn3\",\"path\":\"EntityVersion\"}},\"children\":[],\"ref\":\"input4\"},{\"id\":\"cmp_msyjplfq_py8lz\",\"type\":\"Input\",\"kind\":\"ui\",\"props\":{\"asElement\":\"\",\"disabled\":false,\"invalid\":false,\"rows\":3,\"prefix\":\"Extra Properties :\",\"size\":\"md\",\"suffix\":\"\",\"textArea\":false,\"type\":\"text\",\"unstyle\":false,\"field\":\"\",\"form\":\"\",\"className\":\"\",\"children\":\"\",\"style\":{},\"placeholder\":\"Metin girin...\",\"value\":\"\",\"name\":\"\",\"autoComplete\":\"off\",\"autoFocus\":false,\"readOnly\":false,\"required\":false,\"maxLength\":\"\",\"minLength\":\"\",\"pattern\":\"\",\"sqlDefaultValue\":\"{}\"},\"events\":{\"onChange\":\"\"},\"bindings\":{\"value\":{\"sourceId\":\"cmp_msyfmegt_jkkn3\",\"path\":\"ExtraProperties\"}},\"children\":[],\"ref\":\"input5\"}],\"ref\":\"form1\"},{\"id\":\"cmp_mt023r5u_rb2jv\",\"type\":\"Grid\",\"kind\":\"ui\",\"props\":{\"hoverable\":true,\"overflow\":true,\"borderlessRow\":false,\"compact\":false,\"cols\":\"3\",\"rows\":\"2\",\"gap\":\"4\",\"colGap\":\"4\",\"rowGap\":\"4\",\"responsive\":true,\"smCols\":\"1\",\"mdCols\":\"2\",\"lgCols\":\"3\",\"xlCols\":\"4\",\"autoFit\":false,\"minColWidth\":\"200px\",\"height\":\"auto\",\"className\":\"mt-4\",\"items\":[{\"id\":\"grid-item-1\",\"content\":\"Grid Item 1\",\"colSpan\":1,\"rowSpan\":1,\"colStart\":1,\"rowStart\":1,\"className\":\"bg-blue-100 p-4 border rounded\"},{\"id\":\"grid-item-2\",\"content\":\"Grid Item 2\",\"colSpan\":1,\"rowSpan\":1,\"colStart\":2,\"rowStart\":1,\"className\":\"bg-green-100 p-4 border rounded\"},{\"id\":\"grid-item-3\",\"content\":\"Grid Item 3\",\"colSpan\":1,\"rowSpan\":1,\"colStart\":3,\"rowStart\":1,\"className\":\"bg-yellow-100 p-4 border rounded\"},{\"id\":\"grid-item-4\",\"content\":\"Grid Item 4\",\"colSpan\":2,\"rowSpan\":1,\"colStart\":1,\"rowStart\":2,\"className\":\"bg-purple-100 p-4 border rounded\"},{\"id\":\"grid-item-5\",\"content\":\"Grid Item 5\",\"colSpan\":1,\"rowSpan\":1,\"colStart\":3,\"rowStart\":2,\"className\":\"bg-red-100 p-4 border rounded\"}],\"dataColumns\":[\"UserId\"],\"columnLookups\":{\"UserId\":{\"sourceId\":\"source_gq_0q1dz\",\"path\":\"\",\"valueField\":\"Id\",\"textField\":\"UserName\"}},\"columnCaptions\":{}},\"events\":{},\"bindings\":{\"items\":{\"sourceId\":\"source_tq_veb4m\",\"path\":\"\"}},\"children\":[],\"ref\":\"grid1\"},{\"id\":\"cmp_mt0288i3_ebpj5\",\"type\":\"ListView\",\"kind\":\"platform\",\"props\":{\"listFormCode\":\"AbpIdentity.Roles\",\"height\":\"520px\",\"filters\":[{\"id\":\"filter_y9_tr73w\",\"field\":\"Id\",\"operator\":\"eq\",\"source\":\"record\",\"value\":\"form1.Id\",\"previewValue\":\"\",\"required\":true}],\"className\":\"mt-4\"},\"events\":{},\"bindings\":{},\"children\":[],\"ref\":\"listView1\"}],\"canvas\":{\"width\":\"responsive\"},\"lifecycle\":{\"onMount\":\"\"},\"dataSources\":[{\"id\":\"source_5w_5ibpv\",\"name\":\"AbpRoles · GetList\",\"method\":\"GET\",\"url\":\"/api/app/crudendpoint/AbpRoles\",\"responsePath\":\"\"},{\"id\":\"source_i7_msox4\",\"name\":\"AbpRoles · GetById\",\"method\":\"GET\",\"url\":\"/api/app/crudendpoint/AbpRoles/{id}\",\"responsePath\":\"\"},{\"id\":\"source_0t_avok9\",\"name\":\"AbpRoles · Delete\",\"method\":\"DELETE\",\"url\":\"/api/app/crudendpoint/AbpRoles/{id}\",\"responsePath\":\"\"},{\"id\":\"source_h3_484qa\",\"name\":\"AbpRoles · Create\",\"method\":\"POST\",\"url\":\"/api/app/crudendpoint/AbpRoles\",\"responsePath\":\"\"},{\"id\":\"source_gb_d9cvs\",\"name\":\"AbpRoles · Update\",\"method\":\"PUT\",\"url\":\"/api/app/crudendpoint/AbpRoles/{id}\",\"responsePath\":\"\"},{\"id\":\"source_tq_veb4m\",\"name\":\"AbpUserRoles · GetList\",\"method\":\"GET\",\"url\":\"/api/app/crudendpoint/AbpUserRoles\",\"responsePath\":\"\",\"filters\":[{\"id\":\"filter_sw_86e0q\",\"field\":\"RoleId\",\"operator\":\"eq\",\"source\":\"record\",\"value\":\"form1.id\",\"previewValue\":\"\",\"required\":true}]},{\"id\":\"source_d2_bri5s\",\"name\":\"AbpUserRoles · GetById\",\"method\":\"GET\",\"url\":\"/api/app/crudendpoint/AbpUserRoles/{id}\",\"responsePath\":\"\"},{\"id\":\"source_gq_0q1dz\",\"name\":\"AbpUsers · GetList\",\"method\":\"GET\",\"url\":\"/api/app/crudendpoint/AbpUsers\",\"responsePath\":\"\"},{\"id\":\"source_u8_nu1lu\",\"name\":\"AbpUsers · GetById\",\"method\":\"GET\",\"url\":\"/api/app/crudendpoint/AbpUsers/{id}\",\"responsePath\":\"\"}],\"permissionCode\":\"\"}}", "DataSources": [ { "Name": "AbpRoles · GetList", diff --git a/configs/seeds/host/wizard/RoleList.json b/configs/seeds/host/wizard/RoleList.json new file mode 100644 index 00000000..5a0b6a80 --- /dev/null +++ b/configs/seeds/host/wizard/RoleList.json @@ -0,0 +1,159 @@ +{ + "Wizard": { + "ComponentKind": 1, + "WizardName": "RoleList", + "WizardCode": "App.Wizard.RoleList", + "SeededAt": "2026-09-05T19:53:59.1764998\u002B03:00", + "CustomComponentName": "RoleComponent", + "PermissionGroupName": "App.Wizard.Sales", + "ModuleId": "Sal", + "Menu": { + "MenuParentCode": "App.Wizard.Sales", + "MenuParentIcon": "FcAssistant", + "MenuCode": "App.Wizard.RoleList", + "MenuOrder": 2, + "MenuUrl": "/admin/roles", + "MenuIcon": "FcBullish" + }, + "Language": { + "MenuParentEn": "Sales", + "MenuParentTr": "Sat\u0131\u015F", + "PermissionGroupEn": "Sales", + "PermissionGroupTr": "Sat\u0131\u015F", + "MenuEn": "Role List", + "MenuTr": "Role List", + "TitleEn": "Role List", + "TitleTr": "Role List", + "DescEn": "Role List", + "DescTr": "Role List" + }, + "Groups": [], + "IsTenant": false, + "IsBranch": false, + "IsOrganizationUnit": false, + "AllowAdding": true, + "AllowUpdating": true, + "AllowDeleting": true, + "AllowDetail": false, + "ConfirmDelete": true, + "DefaultLayout": "grid", + "Grid": true, + "Card": true, + "Pivot": true, + "Tree": false, + "Chart": true, + "Gantt": false, + "Scheduler": false, + "Todo": false, + "DataSourceCode": "Default", + "SelectCommandType": 1, + "SelectCommand": "", + "KeyFieldName": "", + "KeyFieldDbSourceType": 11, + "TreeOptionDto": { + "KeyExpr": "", + "ParentIdExpr": "", + "HasItemsExpr": "", + "RootValue": "", + "ExpandedRowKeys": [], + "AutoExpandAll": false, + "RecursiveSelection": false, + "TitleExpr": null, + "StartExpr": null, + "EndExpr": null, + "ProgressExpr": null + }, + "GanttOptionDto": { + "KeyExpr": "", + "ParentIdExpr": "", + "HasItemsExpr": "", + "RootValue": "", + "ExpandedRowKeys": [], + "AutoExpandAll": false, + "RecursiveSelection": false, + "TitleExpr": "", + "ScaleType": "weeks", + "StartExpr": "", + "EndExpr": "", + "ProgressExpr": "", + "EditingEnabled": false, + "AllowTaskAdding": false, + "AllowTaskUpdating": false, + "AllowTaskResourceUpdating": false, + "AllowTaskDeleting": false, + "AllowDependencyAdding": false, + "AllowDependencyDeleting": false, + "AllowResourceAdding": false, + "AllowResourceUpdating": false, + "AllowResourceDeleting": false + }, + "SchedulerOptionDto": { + "TextExpr": "", + "UserNameExpr": null, + "DescriptionExpr": null, + "StartDateExpr": "", + "EndDateExpr": "", + "AllDayExpr": "", + "RecurrenceRuleExpr": "", + "RecurrenceExceptionExpr": "", + "StartDayHour": 8, + "EndDayHour": 20, + "DefaultView": "week", + "ShowAllDayPanel": true, + "CellDuration": 30, + "FirstDayOfWeek": 1, + "CrossScrollingEnabled": false, + "AllowResizing": false, + "AllowDragging": false, + "AllowDeleting": false, + "AllowEditing": false, + "AllowAdding": false + }, + "TodoOptionDto": { + "TitleExpr": "", + "StatusExpr": "", + "DescriptionExpr": "", + "DueDateExpr": "", + "TagExpr": "", + "AssigneeExpr": "", + "PriorityExpr": "", + "CompletedExpr": "", + "OrderExpr": "", + "TimesheetExpr": null, + "SubTaskExpr": null, + "StatusOrder": "Backlog, Ready to Start, In Progress, Code Review, Testing, Staging", + "AllowDragging": true + }, + "WorkflowDto": { + "ApprovalUserFieldName": "", + "ApprovalDateFieldName": "", + "ApprovalStatusFieldName": "", + "ApprovalDescriptionFieldName": "", + "ApprovalIsFilterUserName": false, + "ApprovalIsResetWorkflow": false, + "Criteria": [] + }, + "SubForms": [], + "Widgets": [] + }, + "ListForm": null, + "Fields": [], + "IsDeletedField": false, + "IsCreatedField": false, + "InsertedRecords": { + "LanguageKeys": [ + "App.Wizard.RoleList" + ], + "PermissionGroupNames": [], + "PermissionNames": [ + "App.Wizard.RoleList", + "App.Wizard.RoleList.Create", + "App.Wizard.RoleList.Update", + "App.Wizard.RoleList.Delete" + ], + "MenuCodes": [ + "App.Wizard.RoleList" + ], + "DataSourceCodes": [] + } +} \ No newline at end of file diff --git a/ui/src/components/layouts/DeveloperLayout.tsx b/ui/src/components/layouts/DeveloperLayout.tsx deleted file mode 100644 index 3b22c8b5..00000000 --- a/ui/src/components/layouts/DeveloperLayout.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import type { PropsWithChildren } from 'react' -import { Container } from '../shared' - -const DeveloperLayout = ({ children }: PropsWithChildren) => { - return ( - -
-
{children}
-
-
- ) -} - -export default DeveloperLayout diff --git a/ui/src/constants/permission.constant.ts b/ui/src/constants/permission.constant.ts index 869d7eff..c2785654 100644 --- a/ui/src/constants/permission.constant.ts +++ b/ui/src/constants/permission.constant.ts @@ -31,19 +31,6 @@ export const WIZARD_PERMISSION = { IMPORT: 'App.Listforms.Wizard.Import', } as const -/** - * Custom component yöneticisi yetkileri. `PermissionsData.json` içinde tanımlıdır ve seeder ile - * yönetilir; buradaki kontroller yalnızca butonları gizler, asıl kontrol - * `CustomComponentAppService` üzerindeki ABP policy adlarındadır. - */ -export const COMPONENT_PERMISSION = { - /** Ekranı görme (okuma) yetkisi; alt yetkilerin tamamının üstüdür. */ - DEFAULT: 'App.DeveloperKit.Components', - CREATE: 'App.DeveloperKit.Components.Create', - UPDATE: 'App.DeveloperKit.Components.Update', - DELETE: 'App.DeveloperKit.Components.Delete', -} as const - /** * ListForm alan (Fields) yoneticisi yetkileri. `PermissionsData.json` icinde tanimlidir ve * seeder ile yonetilir; buradaki kontroller yalnizca butonlari kapatir, asil kontrol diff --git a/ui/src/contexts/ComponentContext.tsx b/ui/src/contexts/ComponentContext.tsx index 7d2cb0f2..9b2de4df 100644 --- a/ui/src/contexts/ComponentContext.tsx +++ b/ui/src/contexts/ComponentContext.tsx @@ -23,7 +23,6 @@ import { buildRuntimeScope, compileComponentBundle, getCompilationSignature, - toErrorMessage, type BabelLike, type RuntimeComponent, } from './componentRuntime' @@ -34,19 +33,12 @@ interface ComponentProps { interface ComponentContextType { components: CustomComponent[] - loading: boolean - error: string | null /** True while the runtime bundle is being (re)compiled. */ compiling: boolean /** Compilation/initialization errors keyed by component name. */ componentErrors: Record - addComponent: (component: CreateUpdateCustomComponentDto) => Promise updateComponent: (id: string, component: CreateUpdateCustomComponentDto) => Promise - deleteComponent: (id: string) => Promise - getComponent: (id: string) => CustomComponent | undefined getComponentByName: (name: string) => CustomComponent | undefined - refreshComponents: () => Promise - refreshComponent: (id: string) => Promise /** Natively implemented components exposed to dynamic routes by name. */ registeredComponents: Record> registerComponent: (name: string, component: React.ComponentType) => void @@ -103,8 +95,6 @@ export const ComponentProvider: React.FC<{ children: React.ReactNode }> = ({ chi const extraProperties = applicationConfig?.extraProperties const [components, setComponents] = useState([]) - const [loading, setLoading] = useState(false) - const [error, setError] = useState(null) const [compiling, setCompiling] = useState(false) const [registeredComponents, setRegisteredComponents] = useState>>(EMPTY_NATIVE_REGISTRY) @@ -145,41 +135,16 @@ export const ComponentProvider: React.FC<{ children: React.ReactNode }> = ({ chi if (!applicationConfig) return try { - setError(null) const result = await developerKitService.getCustomComponents() const nextComponents = result.items || [] setComponents((current) => haveSameComponentRevision(current, nextComponents) ? current : nextComponents, ) } catch (err) { - setError(toErrorMessage(err)) console.error('Failed to fetch components:', err) } }, [applicationConfig]) - const refreshComponent = useCallback( - async (id: string): Promise => { - if (!applicationConfig) return undefined - - try { - setError(null) - const freshComponent = await developerKitService.getCustomComponent(id) - setComponents((current) => { - const exists = current.some((component) => component.id === id) - return exists - ? current.map((component) => (component.id === id ? freshComponent : component)) - : [...current, freshComponent] - }) - return freshComponent - } catch (err) { - setError(toErrorMessage(err)) - console.error('Failed to fetch component:', err) - return undefined - } - }, - [applicationConfig], - ) - // Components delivered with the application configuration are the initial set. useEffect(() => { const initialComponents = @@ -197,60 +162,17 @@ export const ComponentProvider: React.FC<{ children: React.ReactNode }> = ({ chi return () => componentsChangedChannel?.removeEventListener('message', handleComponentsChanged) }, [refreshComponents]) - const addComponent = useCallback(async (componentData: CreateUpdateCustomComponentDto) => { - try { - setLoading(true) - setError(null) - const newComponent = await developerKitService.createCustomComponent(componentData) - setComponents((prev) => [...prev, newComponent]) - notifyComponentsChanged() - } catch (err) { - setError(toErrorMessage(err)) - throw err - } finally { - setLoading(false) - } - }, []) - const updateComponent = useCallback( async (id: string, componentData: CreateUpdateCustomComponentDto) => { - try { - setLoading(true) - setError(null) - const updatedComponent = await developerKitService.updateCustomComponent(id, componentData) - setComponents((prev) => - prev.map((component) => (component.id === id ? updatedComponent : component)), - ) - notifyComponentsChanged() - } catch (err) { - setError(toErrorMessage(err)) - throw err - } finally { - setLoading(false) - } + const updatedComponent = await developerKitService.updateCustomComponent(id, componentData) + setComponents((prev) => + prev.map((component) => (component.id === id ? updatedComponent : component)), + ) + notifyComponentsChanged() }, [], ) - const deleteComponent = useCallback(async (id: string) => { - try { - setLoading(true) - setError(null) - await developerKitService.deleteCustomComponent(id) - setComponents((prev) => prev.filter((component) => component.id !== id)) - notifyComponentsChanged() - } catch (err) { - setError(toErrorMessage(err)) - throw err - } finally { - setLoading(false) - } - }, []) - - const componentsById = useMemo( - () => new Map(components.map((component) => [component.id, component])), - [components], - ) const componentsByName = useMemo( () => new Map(components.map((component) => [component.name, component])), [components], @@ -271,8 +193,6 @@ export const ComponentProvider: React.FC<{ children: React.ReactNode }> = ({ chi [compilableComponents], ) - const getComponent = useCallback((id: string) => componentsById.get(id), [componentsById]) - const getComponentByName = useCallback( (name: string) => componentsByName.get(name), [componentsByName], @@ -397,17 +317,10 @@ export const ComponentProvider: React.FC<{ children: React.ReactNode }> = ({ chi const value = useMemo( () => ({ components, - loading, - error, compiling, componentErrors, - addComponent, updateComponent, - deleteComponent, - getComponent, getComponentByName, - refreshComponents, - refreshComponent, registeredComponents, registerComponent, renderComponent, @@ -415,17 +328,10 @@ export const ComponentProvider: React.FC<{ children: React.ReactNode }> = ({ chi }), [ components, - loading, - error, compiling, componentErrors, - addComponent, updateComponent, - deleteComponent, - getComponent, getComponentByName, - refreshComponents, - refreshComponent, registeredComponents, registerComponent, renderComponent, diff --git a/ui/src/contexts/componentRuntime.ts b/ui/src/contexts/componentRuntime.ts index 6f5546a8..495f93a3 100644 Binary files a/ui/src/contexts/componentRuntime.ts and b/ui/src/contexts/componentRuntime.ts differ diff --git a/ui/src/proxy/admin/wizard/models.ts b/ui/src/proxy/admin/wizard/models.ts index e0464c1c..126c0f52 100644 --- a/ui/src/proxy/admin/wizard/models.ts +++ b/ui/src/proxy/admin/wizard/models.ts @@ -192,7 +192,7 @@ export enum WizardImportEntryStatusEnum { } export interface WizardImportEntryDto { - /** Kapsam klasorune goreli yol, ornegin `custom/Orders.json`. */ + /** Kapsam klasorune goreli yol, ornegin `crud/Orders.json`. */ path: string folder: string fileName: string @@ -213,6 +213,28 @@ export interface WizardImportAnalysisDto { warnings: string[] } +/** + * Import kapanisinin sonucu. Yazilan `data/*.json` dosyalari ayni islemde veritabanina da + * uygulanir; her dosyanin sonucu burada raporlanir. + */ +export interface WizardImportCompleteResultDto { + appliedData: WizardImportSeedApplyResultDto[] +} + +/** Tek bir veri seed dosyasinin veritabanina uygulanma sonucu. */ +export interface WizardImportSeedApplyResultDto { + /** Kapsam klasorune goreli yol. */ + path: string + /** Veritabanina eklenen satir sayisi. */ + insertedRowCount: number + /** Zaten var oldugu icin dosyadaki degerlerle guncellenen satir sayisi. */ + updatedRowCount: number + /** Anahtari olmadigi icin guncellenemeyen, dokunulmayan satir sayisi. */ + skippedRowCount: number + /** Dosya uygulanamadiysa nedeni; basarili durumda bostur. */ + error?: string +} + export interface WizardImportApplyResultDto { path: string overwritten: boolean diff --git a/ui/src/routes/route.constant.ts b/ui/src/routes/route.constant.ts index 73df520f..04076b4a 100644 --- a/ui/src/routes/route.constant.ts +++ b/ui/src/routes/route.constant.ts @@ -39,8 +39,7 @@ entities: '/admin/developerkit/entities', entitiesNew: '/admin/developerkit/entities/new', entitiesEdit: '/admin/developerkit/entities/edit/:id', - components: '/admin/developerkit/components', - componentsEdit: '/admin/developerkit/components/edit/:id', + components: '/admin/list/App.DeveloperKit.CustomComponents', }, reports: { view: '/admin/reports/:report/view/:id?/:listFormCode?', diff --git a/ui/src/services/developerKit.service.ts b/ui/src/services/developerKit.service.ts index 53f763da..a43435d7 100644 --- a/ui/src/services/developerKit.service.ts +++ b/ui/src/services/developerKit.service.ts @@ -18,14 +18,6 @@ class DeveloperKitService { return response.data } - async getActiveCustomComponents(): Promise> { - const response = await apiService.fetchData>({ - url: '/api/app/custom-component/active-components', - method: 'GET', - }) - return response.data - } - async getCustomComponent(id: string): Promise { const response = await apiService.fetchData({ url: `/api/app/custom-component/${id}`, @@ -34,15 +26,6 @@ class DeveloperKitService { return response.data } - async createCustomComponent(component: CreateUpdateCustomComponentDto): Promise { - const response = await apiService.fetchData({ - url: '/api/app/custom-component', - method: 'POST', - data: component as any, - }) - return response.data - } - async updateCustomComponent( id: string, component: CreateUpdateCustomComponentDto, @@ -59,13 +42,6 @@ class DeveloperKitService { return response.data } - async deleteCustomComponent(id: string): Promise { - await apiService.fetchData({ - url: `/api/app/custom-component/${id}`, - method: 'DELETE', - }) - } - // Generated Endpoint endpoints async toggleGeneratedEndpoint(id: string): Promise { const response = await apiService.fetchData({ diff --git a/ui/src/services/wizard.service.ts b/ui/src/services/wizard.service.ts index 8fe13c57..6951fa22 100644 --- a/ui/src/services/wizard.service.ts +++ b/ui/src/services/wizard.service.ts @@ -3,6 +3,7 @@ import { WizardFileInfoDto, WizardImportAnalysisDto, WizardImportApplyResultDto, + WizardImportCompleteResultDto, WizardPermissionGroupDto, WizardSeedFileDto, } from '@/proxy/admin/wizard/models' @@ -43,7 +44,7 @@ export const deleteWizardFile = (fileName: string) => }) /** - * Wizard dosyasini ve bagli seed dosyalarini (custom/crud/sql) tek bir zip olarak indirir. + * Wizard dosyasini ve bagli seed dosyalarini (data/crud/sql) tek bir zip olarak indirir. * Zip icindeki klasor duzeni seed kapsam klasoru ile aynidir; baska bir ortamda dogrudan acilabilir. */ export const exportWizardFile = (fileName: string) => @@ -93,9 +94,12 @@ export const applyWizardImport = (importId: string, path: string, content?: stri data: { importId, path, content: content ?? null }, }) -/** Import oturumunu kapatir; staging ve yedek klasorleri silinir. */ +/** + * Import oturumunu kapatir; staging ve yedek klasorleri silinir. Kapanistan once yazilan + * `data/*.json` dosyalari veritabanina uygulanir ve sonucu doner. + */ export const completeWizardImport = (importId: string) => - apiService.fetchData({ + apiService.fetchData({ method: 'POST', url: `/api/app/list-form-wizard/complete-import`, params: { importId }, diff --git a/ui/src/views/admin/listForm/wizard/WizardFileManager.tsx b/ui/src/views/admin/listForm/wizard/WizardFileManager.tsx index 6bfca1ee..7f3ee851 100644 --- a/ui/src/views/admin/listForm/wizard/WizardFileManager.tsx +++ b/ui/src/views/admin/listForm/wizard/WizardFileManager.tsx @@ -3,6 +3,8 @@ import { useNavigate } from 'react-router-dom' import classNames from 'classnames' import { Button, Dialog, Input, Notification, toast } from '@/components/ui' import Container from '@/components/shared/Container' +import Loading from '@/components/shared/Loading' +import PageTitle from '@/components/shared/PageTitle' import { FaTrash, FaSync, @@ -234,6 +236,8 @@ const WizardFileManager = () => { return ( + + {/* ── Header ─────────────────────────────────────────────── */}
{ > + {canImport && ( <>
-
+ {filteredFiles.length === 0 && !loading && (
@@ -338,12 +346,6 @@ const WizardFileManager = () => {
)} - {loading && ( -

- {translate('::App.Platform.LoadingWithThreeDot')} -

- )} - {viewMode === 'card' ? (
{filteredFiles.map((f) => { @@ -511,7 +513,7 @@ const WizardFileManager = () => {
)} -
+ {importFile && ( { return `${(size / 1024).toFixed(1)} KB` } +/** + * Dialog gövdesi. Normalde yüksekliği viewport'a göre sınırlanır; büyütülmüş (maximize) modda + * dialog zaten `100vh` olduğu için gövde esner ve footer en altta, görünür kalır — sabit yükseklik + * bırakılırsa footer ekranın dışına taşar. `useDialogContext` yalnızca `Dialog` ağacının içinde + * çalıştığından bu ayrı bir bileşendir. + */ +const ImportDialogBody = ({ children }: { children: ReactNode }) => { + const { isMaximized } = useDialogContext() + + return ( + + {children} + + ) +} + /** * Wizard export zip'ini kapsam klasorune aktarir. * Akis: analiz → catisma cozumu (diff) → toplu yazma. Yazma sirasinda bir hata olursa @@ -84,6 +110,8 @@ const WizardImportDialog = ({ file, onClose, onImported }: WizardImportDialogPro const [decisions, setDecisions] = useState>({}) const [selectedPath, setSelectedPath] = useState(null) const [applied, setApplied] = useState([]) + // Yazilan data/*.json dosyalarinin veritabanina uygulanma sonucu; oturum kapanisinda doner. + const [seedApplied, setSeedApplied] = useState([]) const [currentPath, setCurrentPath] = useState(null) const [showIdentical, setShowIdentical] = useState(false) const [editorEpoch, setEditorEpoch] = useState(0) @@ -228,6 +256,7 @@ const WizardImportDialog = ({ file, onClose, onImported }: WizardImportDialogPro setPhase('running') setApplied([]) + setSeedApplied([]) setError(null) const rows: AppliedRow[] = [] @@ -267,9 +296,11 @@ const WizardImportDialog = ({ file, onClose, onImported }: WizardImportDialogPro setCurrentPath(null) try { - await completeWizardImport(analysis.importId) - } catch { - // Oturum klasoru zaten gecici; temizlenemezse import basarili sayilir. + const res = await completeWizardImport(analysis.importId) + setSeedApplied(res.data.appliedData ?? []) + } catch (err: any) { + // Dosyalar hedefe yazildi; yalnizca veritabanina uygulama ya da temizlik basarisiz oldu. + setError(err?.response?.data?.error?.message ?? err?.message ?? null) } setPhase('finished') onImported() @@ -565,7 +596,37 @@ const WizardImportDialog = ({ file, onClose, onImported }: WizardImportDialogPro {translate('::App.ListForm.WizardImportSuccess')} + {seedApplied.length > 0 && ( +
+
+ {translate('::App.ListForm.WizardImportDataApplied')} +
+
    + {seedApplied.map((row) => ( +
  • + {row.path} + + {row.error ?? + translate('::App.ListForm.WizardImportDataRowSummary', { + inserted: row.insertedRowCount, + updated: row.updatedRowCount, + skipped: row.skippedRowCount, + })} + +
  • + ))} +
+
+ )}
{translate('::App.ListForm.WizardImportRunMigrateHint')}
+ {error && ( +
+ + + {translate('::App.ListForm.WizardImportDataApplyFailed')} {error} + +
+ )} )} @@ -581,7 +642,7 @@ const WizardImportDialog = ({ file, onClose, onImported }: WizardImportDialogPro onClose={handleClose} onRequestClose={handleClose} > - +
{translate('::App.ListForm.WizardImportTitle')}
@@ -622,7 +683,7 @@ const WizardImportDialog = ({ file, onClose, onImported }: WizardImportDialogPro {(phase === 'running' || phase === 'finished' || phase === 'rolledBack') && renderProgress()} - + {phase === 'review' && !error && unresolvedCount > 0 && ( diff --git a/ui/src/views/developerKit/ComponentEditor.tsx b/ui/src/views/developerKit/ComponentEditor.tsx deleted file mode 100644 index 723b02ec..00000000 --- a/ui/src/views/developerKit/ComponentEditor.tsx +++ /dev/null @@ -1,297 +0,0 @@ -import React, { useState, useEffect } from 'react' -import { useComponents } from '../../contexts/ComponentContext' -import { FaRegSave, FaSync, FaCog } from 'react-icons/fa' -import { useLocalization } from '@/utils/hooks/useLocalization' -import { Formik, Form, Field } from 'formik' -import * as Yup from 'yup' -import { Button, Checkbox, FormContainer, FormItem, Input } from '@/components/ui' -import { usePermission } from '@/utils/hooks/usePermission' -import { COMPONENT_PERMISSION } from '@/constants/permission.constant' -import Dialog from '@/components/ui/Dialog' -import { generateDesignerCode } from '@/components/visualDesigner/codeGenerator' -import { - createDesignerId, - createEmptyDesignerDocument, - type DesignerDocument, -} from '@/components/visualDesigner/types' - -// Validation schema -const validationSchema = Yup.object({ - name: Yup.string().required(), - routePath: Yup.string().required().matches(/^\//, 'Route path must start with /'), - description: Yup.string(), - dependencies: Yup.array().of(Yup.string()), - isActive: Yup.boolean(), -}) - -interface ComponentEditorProps { - componentId?: string | null - onClose?: () => void - onSaved?: () => void | Promise -} - -const ComponentEditor: React.FC = ({ - componentId, - onClose, - onSaved, -}) => { - const id = componentId || undefined - const { translate } = useLocalization() - - const { getComponent, addComponent, updateComponent, refreshComponent } = useComponents() - - const [isLoaded, setIsLoaded] = useState(false) - - const isEditing = !!id - - // Kaydetme hakki moda gore degisir: mevcut kayit guncelleme, yeni kayit olusturma yetkisi ister. - const { checkPermission } = usePermission() - const canSave = checkPermission( - isEditing ? COMPONENT_PERMISSION.UPDATE : COMPONENT_PERMISSION.CREATE, - ) - - // Initial values for Formik - const [initialValues, setInitialValues] = useState({ - name: '', - routePath: '', - description: '', - dependencies: [] as string[], - isActive: true, - }) - - // Load existing component data - sadece edit modunda - useEffect(() => { - if (isEditing && id && !isLoaded) { - let cancelled = false - - void refreshComponent(id).then((freshComponent) => { - if (cancelled) return - const component = freshComponent - if (!component) { - setIsLoaded(true) - return - } - - // Parse dependencies from JSON string - let deps: string[] = [] - try { - deps = component.dependencies ? JSON.parse(component.dependencies) : [] - deps = Array.isArray(deps) ? deps : [] - } catch { - deps = [] - } - - const values = { - name: component.name, - routePath: component.routePath, - description: component.description || '', - dependencies: deps, - isActive: component.isActive, - } - - setInitialValues(values) - setIsLoaded(true) - }) - - return () => { - cancelled = true - } - } else if (!isEditing && !isLoaded) { - // Yeni komponent için boş başla - TEMPLATE YOK - setIsLoaded(true) - } - }, [id, isEditing, isLoaded, refreshComponent]) - - const createDefaultVisualDocument = (componentName: string): DesignerDocument => { - const document = createEmptyDesignerDocument('visual') - document.nodes = [ - { - id: createDesignerId(), - type: 'span', - kind: 'html', - props: { children: componentName.trim() }, - events: {}, - bindings: {}, - children: [], - }, - ] - return document - } - - const handleSubmit = async (values: typeof initialValues, { setSubmitting }: any) => { - if (!values.name.trim()) { - alert('Please enter a component name') - setSubmitting(false) - return - } - - try { - const existingComponent = isEditing && id ? getComponent(id) : undefined - const defaultDocument = createDefaultVisualDocument(values.name) - const componentData = { - name: values.name.trim(), - routePath: values.routePath.trim(), - description: values.description.trim(), - dependencies: JSON.stringify(values.dependencies), // Serialize dependencies to JSON string - code: existingComponent - ? existingComponent.code - : generateDesignerCode(values.name, defaultDocument), - props: existingComponent - ? existingComponent.props - : JSON.stringify({ visualDesigner: defaultDocument }), - isActive: values.isActive, - } - - if (isEditing && id) { - await updateComponent(id, componentData) - } else { - await addComponent(componentData) - } - - await onSaved?.() - onClose?.() - } catch (error) { - console.error('Error saving component:', error) - alert('Failed to save component. Please try again.') - } finally { - setSubmitting(false) - } - } - - // Loading state - komponent yüklenene kadar bekle - if (isEditing && !isLoaded) { - return ( -
-
- -

{translate('::App.Platform.LoadingWithThreeDot')}

-
-
- ) - } - - return ( - - {({ values, touched, errors, isSubmitting, setFieldValue, submitForm, isValid }) => { - return ( - <> -
-
-
-
-
- -
-

- Component Settings -

-
- - - - ) => { - setFieldValue('name', e.target.value) - }} - /> - - - - - - - - - - - - - {() => ( - - setFieldValue( - 'dependencies', - e.target.value - .split(',') - .map((s) => s.trim()) - .filter(Boolean), - ) - } - /> - )} - - - - - - - -
-
-
- -
- - -
-
- - ) - }} -
- ) -} - -export default ComponentEditor diff --git a/ui/src/views/developerKit/ComponentEditorPage.tsx b/ui/src/views/developerKit/ComponentEditorPage.tsx deleted file mode 100644 index 9aee1ca0..00000000 --- a/ui/src/views/developerKit/ComponentEditorPage.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import DeveloperLayout from '@/components/layouts/DeveloperLayout' -import ComponentManager from '@/views/developerKit/ComponentManager' - -// Existing dynamic-route records can still reference this physical module. -// Keep the compatibility entry point, but manage create/view operations in the manager modal. -const ComponentEditorPage = () => ( - - - -) - -export default ComponentEditorPage diff --git a/ui/src/views/developerKit/ComponentManager.tsx b/ui/src/views/developerKit/ComponentManager.tsx deleted file mode 100644 index 0f4d9186..00000000 --- a/ui/src/views/developerKit/ComponentManager.tsx +++ /dev/null @@ -1,505 +0,0 @@ -import React, { useEffect, useState } from 'react' -import { useComponents } from '../../contexts/ComponentContext' -import { - FaPlus, - FaSearch, - FaRegEdit, - FaTrashAlt, - FaEye, - FaEyeSlash, - FaFilter, - FaCalendarAlt, - FaExternalLinkAlt, - FaCog, - FaSyncAlt, - FaTh, - FaList, -} from 'react-icons/fa' -import Widget from '@/components/common/Widget' -import { ROUTES_ENUM } from '@/routes/route.constant' -import { useLocalization } from '@/utils/hooks/useLocalization' -import { Loading } from '../../components/shared' -import PageTitle from '@/components/shared/PageTitle' -import { Button } from '@/components/ui' -import Dialog from '@/components/ui/Dialog' -import ComponentEditor from './ComponentEditor' -import { parseComponentDependencies } from '@/contexts/componentRuntime' -import { usePermission } from '@/utils/hooks/usePermission' -import { COMPONENT_PERMISSION } from '@/constants/permission.constant' -import Input from '@/components/ui/Input' -import Select from '@/components/ui/Select' -import type { CustomComponent } from '@/proxy/developerKit/models' -import { useStoreActions, useStoreState } from '@/store/store' -import type { ListViewLayoutType } from '../admin/listForm/edit/types' -import { ACTION_BUTTON_CLASS, ACTION_BUTTON_HOVER } from '@/constants/actionButton.constant' - -/** Layout tercihi diger listelerle ayni yerde (admin.lists.states) saklanir. */ -const VIEW_STATE_CODE = 'developerkit-components' - -const ComponentManager: React.FC = () => { - const { - components, - loading, - componentErrors, - updateComponent, - deleteComponent, - refreshComponents, - } = useComponents() - const [searchTerm, setSearchTerm] = useState('') - const [filterActive, setFilterActive] = useState<'all' | 'active' | 'inactive'>('all') - const [isRefreshing, setIsRefreshing] = useState(false) - const [editorComponentId, setEditorComponentId] = useState(undefined) - const { states } = useStoreState((state) => state.admin.lists) - const { setStates } = useStoreActions((a) => a.admin.lists) - const viewMode: ListViewLayoutType = - states.find((s) => s.listFormCode === VIEW_STATE_CODE)?.layout === 'grid' ? 'grid' : 'card' - const setViewMode = (layout: ListViewLayoutType) => - setStates({ listFormCode: VIEW_STATE_CODE, layout }) - - // Calculate statistics - const totalComponents = components?.length || 0 - const activeComponents = components?.filter((c) => c.isActive).length || 0 - const inactiveComponents = totalComponents - activeComponents - const { translate } = useLocalization() - - const filterActiveOptions = [ - { value: 'all' as const, label: translate('::App.ComponentFilter.All') }, - { value: 'active' as const, label: translate('::App.EntityFilter.Active') }, - { value: 'inactive' as const, label: translate('::App.EntityFilter.Inactive') }, - ] - const { checkPermission } = usePermission() - const canCreate = checkPermission(COMPONENT_PERMISSION.CREATE) - const canUpdate = checkPermission(COMPONENT_PERMISSION.UPDATE) - const canDelete = checkPermission(COMPONENT_PERMISSION.DELETE) - - useEffect(() => { - void refreshComponents() - }, [refreshComponents]) - - const handleRefresh = async () => { - setIsRefreshing(true) - try { - await refreshComponents() - } finally { - setIsRefreshing(false) - } - } - - const filteredComponents = components?.filter((component) => { - const matchesSearch = - component.name.toLowerCase().includes(searchTerm.toLowerCase()) || - (component.description || '').toLowerCase().includes(searchTerm.toLowerCase()) - - const matchesFilter = - filterActive === 'all' || - (filterActive === 'active' && component.isActive) || - (filterActive === 'inactive' && !component.isActive) - - return matchesSearch && matchesFilter - }) - - const handleToggleActive = async (id: string, isActive: boolean) => { - try { - const component = components?.find((c) => c.id === id) - if (component) { - await updateComponent(id, { ...component, isActive }) - } - } catch (err) { - console.error('Failed to toggle component status:', err) - } - } - - const handleDelete = async (id: string) => { - if (window.confirm(translate('::App.DeveloperKitComponent.ConfirmDelete'))) { - try { - await deleteComponent(id) - } catch (err) { - console.error('Failed to delete component:', err) - } - } - } - - const renderStatusToggle = (component: CustomComponent) => ( - - ) - - const renderActions = (component: CustomComponent) => ( -
- {canUpdate && ( -
- ) - - return ( -
- - -
- - - -
- - {/* Filters */} -
-
- - setSearchTerm(e.target.value)} - /> -
-
- -