Seeds klasöründe ListFormların klasör olarak saklanması
This commit is contained in:
parent
6af68e6ef5
commit
b31a651313
26 changed files with 790 additions and 263 deletions
5
.github/instructions/ai.instructions.md
vendored
5
.github/instructions/ai.instructions.md
vendored
|
|
@ -295,7 +295,7 @@ the Wizard's `Custom` path.
|
||||||
`deletePermission`). In `Otomatik` mode the base is the read permission the Wizard created for
|
`deletePermission`). In `Otomatik` mode the base is the read permission the Wizard created for
|
||||||
the component and the commands get `''` / `.Create` / `.Update` / `.Delete` suffixes.
|
the component and the commands get `''` / `.Create` / `.Update` / `.Delete` suffixes.
|
||||||
- List screen permissions: `App.DeveloperKit.CustomComponents{,.Create,.Update,.Delete,.Export,.Import}`.
|
- List screen permissions: `App.DeveloperKit.CustomComponents{,.Create,.Update,.Delete,.Export,.Import}`.
|
||||||
- Every save/delete is mirrored into `configs/seeds/{scope}/data/App.DeveloperKit.CustomComponents.json`; the endpoints it uses are
|
- Every save/delete is mirrored into `configs/seeds/{scope}/data/App.DeveloperKit.CustomComponents/{Id}.json`; the endpoints it uses are
|
||||||
referenced through `crud/{Entity}.json`. A component that is not seed-backed is not done.
|
referenced through `crud/{Entity}.json`. A component that is not seed-backed is not done.
|
||||||
|
|
||||||
### 10.2 SQL View Designer
|
### 10.2 SQL View Designer
|
||||||
|
|
@ -910,9 +910,10 @@ own dependencies are collected under the same rules, so the archive stands on it
|
||||||
|
|
||||||
- `wizard/{file}.json` — the screen itself plus the wizard files of the sub-forms it references
|
- `wizard/{file}.json` — the screen itself plus the wizard files of the sub-forms it references
|
||||||
through `SubFormsDto[].Code`, recursively
|
through `SubFormsDto[].Code`, recursively
|
||||||
- `data/App.DeveloperKit.CustomComponents.json` — `Custom` path; the file is filtered down to the
|
- `data/App.DeveloperKit.CustomComponents/{Id}.json` — `Custom` path; the file is filtered down to the
|
||||||
exported component and its dependency chain, never exported whole
|
exported component and its dependency chain, never exported whole
|
||||||
- `data/{ListFormCode}.json` — the screen's own data mirror when `SeedFilePath` is set
|
- `data/{ListFormCode}.json` — the screen's own data mirror when `SeedFilePath` is set
|
||||||
|
(an extensionless `SeedFilePath` is the folder form: `data/{ListFormCode}/{key}.json`, one file per row)
|
||||||
- `crud/{entity}.json` — endpoints those components use
|
- `crud/{entity}.json` — endpoints those components use
|
||||||
- `{sql|postgres}/{object|execute}/{object}.sql` — the object behind `SelectCommand`, the objects
|
- `{sql|postgres}/{object|execute}/{object}.sql` — the object behind `SelectCommand`, the objects
|
||||||
a lookup query (`LookupDto.LookupQuery`) references through `FROM`/`JOIN`, and on the `Custom`
|
a lookup query (`LookupDto.LookupQuery`) references through `FROM`/`JOIN`, and on the `Custom`
|
||||||
|
|
|
||||||
|
|
@ -1172,7 +1172,7 @@ soneklerini alır.
|
||||||
| **Definitions › Skill Type / Skill / Skill Level** | `App.Definitions.SkillType` (+ alt kayıtlar) | `Sas_H_SkillType` · `Sas_H_Skill` · `Sas_H_SkillLevel` | host | Yetkinlik ağacı; `SkillType` kendi kendine ebeveyn olan alt form örneğidir. |
|
| **Definitions › Skill Type / Skill / Skill Level** | `App.Definitions.SkillType` (+ alt kayıtlar) | `Sas_H_SkillType` · `Sas_H_Skill` · `Sas_H_SkillLevel` | host | Yetkinlik ağacı; `SkillType` kendi kendine ebeveyn olan alt form örneğidir. |
|
||||||
| **Definitions › UoM Category / UoM** | `App.Definitions.UomCategory` | `Sas_H_UomCategory` · `Sas_H_Uom` | host | Ölçü birimi, `Ratio`/`Rounding` — miktar alanı olan ekranlarda birim lookup'ı ve çevrim. |
|
| **Definitions › UoM Category / UoM** | `App.Definitions.UomCategory` | `Sas_H_UomCategory` · `Sas_H_Uom` | host | Ölçü birimi, `Ratio`/`Rounding` — miktar alanı olan ekranlarda birim lookup'ı ve çevrim. |
|
||||||
| **Definitions › Sequence** | `App.Definitions.Sequence` | `Adm_T_Sequence` | tenant | Belge numarası üretimi (`lowcode.instructions.md` §9.9); alan varsayılanında `CustomValueType: 5` ile bağlanır. |
|
| **Definitions › Sequence** | `App.Definitions.Sequence` | `Adm_T_Sequence` | tenant | Belge numarası üretimi (`lowcode.instructions.md` §9.9); alan varsayılanında `CustomValueType: 5` ile bağlanır. |
|
||||||
| **Public › Home / About / Services / Contact** | `App.DeveloperKit.CustomComponents` — DeveloperKit > Custom Components (Visual Designer) | `data/App.DeveloperKit.CustomComponents.json` (bileşenin ayrı dosyası yok) | host | Genel siteye açık sayfalar Custom Component'tir; `RoutePath` `/admin/` ile başlamıyorsa otomatik public route olur. Public sayfa isteği kod değil bu tasarımcıdır (README.md §10.4). |
|
| **Public › Home / About / Services / Contact** | `App.DeveloperKit.CustomComponents` — DeveloperKit > Custom Components (Visual Designer) | `data/App.DeveloperKit.CustomComponents/{Id}.json` (her bileşen kendi satır dosyasında) | host | Genel siteye açık sayfalar Custom Component'tir; `RoutePath` `/admin/` ile başlamıyorsa otomatik public route olur. Public sayfa isteği kod değil bu tasarımcıdır (README.md §10.4). |
|
||||||
| **Public › Products / Payment Methods / Installment Options** | `App.Orders.Products` · `.PaymentMethods` · `.InstallmentOptions` | `Sas_H_Product` · `Sas_H_PaymentMethod` · `Sas_H_InstallmentOption` | host | Ürün, ödeme yöntemi (`Commission`), taksit — satış/abonelik ekranlarının lookup kaynağı. |
|
| **Public › Products / Payment Methods / Installment Options** | `App.Orders.Products` · `.PaymentMethods` · `.InstallmentOptions` | `Sas_H_Product` · `Sas_H_PaymentMethod` · `Sas_H_InstallmentOption` | host | Ürün, ödeme yöntemi (`Commission`), taksit — satış/abonelik ekranlarının lookup kaynağı. |
|
||||||
| **Public › Sales Orders** | `App.Orders.SalesOrders` | `Sas_H_Order` + `Sas_H_OrderItem` | host | **Onay akışı + alt kalem + toplam + diyalog** birlikte kullanılan tam örnek (§12.6). |
|
| **Public › Sales Orders** | `App.Orders.SalesOrders` | `Sas_H_Order` + `Sas_H_OrderItem` | host | **Onay akışı + alt kalem + toplam + diyalog** birlikte kullanılan tam örnek (§12.6). |
|
||||||
| **Public › Blog Category / Posts** | `App.BlogManagement.Category` · `.Posts` | `Sas_H_BlogCategory` · `Sas_H_BlogPost` | host | Çok dilli içerik (`ContentTr`/`ContentEn`), slug, yayın durumu, sayaç alanları. Veri `configs/seeds/host/data/App.BlogManagement.Category.json` ve `App.BlogManagement.Posts.json` dosyalarından seed edilir. |
|
| **Public › Blog Category / Posts** | `App.BlogManagement.Category` · `.Posts` | `Sas_H_BlogCategory` · `Sas_H_BlogPost` | host | Çok dilli içerik (`ContentTr`/`ContentEn`), slug, yayın durumu, sayaç alanları. Veri `configs/seeds/host/data/App.BlogManagement.Category.json` ve `App.BlogManagement.Posts.json` dosyalarından seed edilir. |
|
||||||
|
|
@ -1187,7 +1187,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 › 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 › 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 › 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.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 › 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/{Id}.json` klasörüne 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 › 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`. |
|
| **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`. |
|
||||||
| **Developer Kit › MCP Servers** | `App.DeveloperKit.McpServers` · `/admin/list/App.DeveloperKit.McpServers` | `Sas_H_McpServer` | host | `/mcp` ucunun kiracı bazlı sözleşmesi: `Name`, `Url`, `Tenants`, `AllowedTools` (boşsa tüm araçlar, doluysa yalnızca seçilenler açık), `MaxRows`. Araç listesi bu kayda göre süzülür, çağrı anında aynı kural yeniden uygulanır. Yetkiler: ekran için `App.DeveloperKit.McpServers`, araç çağırma için `.Invoke`, veri yazma için `.Write`, prompt ile tablo/ekran/bileşen geliştirme (`create_table`, wizard deploy, custom component kaydı) için `.Develop` (host; Wizard ve Custom Components yetkilerini ayrıca ister). Kiracı kullanıcısı yetkisi ne olursa olsun yalnızca okur (`McpToolNames.TenantTools`: şema araçları, `query_list`, `run_select`); yazma, `create_table`, wizard, component ve `run_query` kiracıya kapalıdır. Host için tek sunucu anahtarı `AllowedTools`. MCP yetki vermez: kullanıcı yalnızca uygulamada okuyabildiği listeleri görür ve sorgular; ham SQL `App.SqlQueryManager` ister. Kayıtlar `data/App.DeveloperKit.McpServers.json` dosyasına senkronlanır. Araçlar ve şema keşfinin kaynağı: `README.md` §13.9. |
|
| **Developer Kit › MCP Servers** | `App.DeveloperKit.McpServers` · `/admin/list/App.DeveloperKit.McpServers` | `Sas_H_McpServer` | host | `/mcp` ucunun kiracı bazlı sözleşmesi: `Name`, `Url`, `Tenants`, `AllowedTools` (boşsa tüm araçlar, doluysa yalnızca seçilenler açık), `MaxRows`. Araç listesi bu kayda göre süzülür, çağrı anında aynı kural yeniden uygulanır. Yetkiler: ekran için `App.DeveloperKit.McpServers`, araç çağırma için `.Invoke`, veri yazma için `.Write`, prompt ile tablo/ekran/bileşen geliştirme (`create_table`, wizard deploy, custom component kaydı) için `.Develop` (host; Wizard ve Custom Components yetkilerini ayrıca ister). Kiracı kullanıcısı yetkisi ne olursa olsun yalnızca okur (`McpToolNames.TenantTools`: şema araçları, `query_list`, `run_select`); yazma, `create_table`, wizard, component ve `run_query` kiracıya kapalıdır. Host için tek sunucu anahtarı `AllowedTools`. MCP yetki vermez: kullanıcı yalnızca uygulamada okuyabildiği listeleri görür ve sorgular; ham SQL `App.SqlQueryManager` ister. Kayıtlar `data/App.DeveloperKit.McpServers.json` dosyasına senkronlanır. Araçlar ve şema keşfinin kaynağı: `README.md` §13.9. |
|
||||||
|
|
@ -1297,7 +1297,7 @@ yazma, oradan yeni bir Custom Component üret. `Services` sayfası yoktur.
|
||||||
|
|
||||||
| Route | Bileşen | Veri |
|
| Route | Bileşen | Veri |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `/home` · `/about` · `/contact` | Custom Component (`PublicHomePage` · `PublicAboutPage` · `PublicContactPage`) | `data/App.DeveloperKit.CustomComponents.json` · tasarımcı: DeveloperKit > Custom Components |
|
| `/home` · `/about` · `/contact` | Custom Component (`PublicHomePage` · `PublicAboutPage` · `PublicContactPage`) | `data/App.DeveloperKit.CustomComponents/{Id}.json` · tasarımcı: DeveloperKit > Custom Components |
|
||||||
| `/products` · `/checkout` · `/payment` · `/success` | `views/public/{Products,Checkout,Payment,Success}` | `Sas_H_Product` · `Sas_H_PaymentMethod` · `Sas_H_InstallmentOption` · `Sas_H_Order` |
|
| `/products` · `/checkout` · `/payment` · `/success` | `views/public/{Products,Checkout,Payment,Success}` | `Sas_H_Product` · `Sas_H_PaymentMethod` · `Sas_H_InstallmentOption` · `Sas_H_Order` |
|
||||||
| `/blog` · `/blog/:id` | `views/public/{Blog,BlogDetail}` | `Sas_H_BlogPost` · `Sas_H_BlogCategory` |
|
| `/blog` · `/blog/:id` | `views/public/{Blog,BlogDetail}` | `Sas_H_BlogPost` · `Sas_H_BlogCategory` |
|
||||||
| `/demo` | `views/public/Demo` | `Sas_H_Demo` |
|
| `/demo` | `views/public/Demo` | `Sas_H_Demo` |
|
||||||
|
|
|
||||||
24
.github/instructions/lowcode.instructions.md
vendored
24
.github/instructions/lowcode.instructions.md
vendored
|
|
@ -55,7 +55,7 @@ configs/seeds/host/
|
||||||
├── wizard/{WizardName}.json ← ekran + menü + yetki + dil anahtarları
|
├── wizard/{WizardName}.json ← ekran + menü + yetki + dil anahtarları
|
||||||
└── data/{ListFormCode}.json ← ekrandan girilen liste verisinin aynası (opsiyonel)
|
└── data/{ListFormCode}.json ← ekrandan girilen liste verisinin aynası (opsiyonel)
|
||||||
Custom Component'ler de burada yaşar:
|
Custom Component'ler de burada yaşar:
|
||||||
data/App.DeveloperKit.CustomComponents.json
|
data/App.DeveloperKit.CustomComponents/{Id}.json
|
||||||
```
|
```
|
||||||
|
|
||||||
Tenant kapsamı isteniyorsa `host/` yerine `tenants/{tenantId}/`; iç düzen aynıdır.
|
Tenant kapsamı isteniyorsa `host/` yerine `tenants/{tenantId}/`; iç düzen aynıdır.
|
||||||
|
|
@ -66,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ı. MCP'de yeni tablo `create_table` ile kurulur (script ve dosya tasarımcıyla aynı); view MCP'den kurulamaz |
|
| 1 | Tablo / view | `sql/object/…` | Ekranın bağlanacağı nesne önce var olmalı. MCP'de yeni tablo `create_table` ile kurulur (script ve dosya tasarımcıyla aynı); view MCP'den kurulamaz |
|
||||||
| 2 | CRUD uçları (gerekiyorsa) | `crud/{Entity}.json` | Custom Component'in data source'ları buna bakar |
|
| 2 | CRUD uçları (gerekiyorsa) | `crud/{Entity}.json` | Custom Component'in data source'ları buna bakar |
|
||||||
| 3 | Custom Component (gerekiyorsa) | `data/App.DeveloperKit.CustomComponents.json` | Wizard `Custom` yolunda bileşen adını arar |
|
| 3 | Custom Component (gerekiyorsa) | `data/App.DeveloperKit.CustomComponents/{Id}.json` | Wizard `Custom` yolunda bileşen adını arar |
|
||||||
| 4 | Wizard | `wizard/{Ad}.json` | Menü, yetki, dil anahtarı ve ListForm'u üretir |
|
| 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 |
|
| 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 |
|
| 6 | Liste verisi (isteniyorsa) | `data/{ListFormCode}.json` | Ekran ve tablo hazır olmalı; `ListForm.SeedFilePath` bu yolu gösterir |
|
||||||
|
|
@ -2030,7 +2030,7 @@ ListForm ile ifade edilemeyen ekranın yolu **fiziksel React sayfası değil**,
|
||||||
|
|
||||||
### 7.1 Seed dosyası
|
### 7.1 Seed dosyası
|
||||||
|
|
||||||
`configs/seeds/{kapsam}/data/App.DeveloperKit.CustomComponents.json` — Custom Components
|
`configs/seeds/{kapsam}/data/App.DeveloperKit.CustomComponents/{Id}.json` — Custom Components
|
||||||
ekranının veri dosyası. Bileşenin **kendi dosyası yoktur**; her bileşen bu dosyada bir satırdır
|
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.
|
ve dosya `data/{ListFormCode}.json` sözleşmesini (§0.1) izler.
|
||||||
|
|
||||||
|
|
@ -2442,7 +2442,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:
|
Bu çift, custom component'in menüye nasıl bağlandığının çalışan örneğidir. İncelenecek dosyalar:
|
||||||
|
|
||||||
```
|
```
|
||||||
configs/seeds/host/data/App.DeveloperKit.CustomComponents.json ← RoleComponent satırı
|
configs/seeds/host/data/App.DeveloperKit.CustomComponents/{Id}.json ← RoleComponent satırı
|
||||||
configs/seeds/host/crud/AbpRoles.json · AbpUsers.json · AbpUserRoles.json
|
configs/seeds/host/crud/AbpRoles.json · AbpUsers.json · AbpUserRoles.json
|
||||||
configs/seeds/host/wizard/RoleList.json
|
configs/seeds/host/wizard/RoleList.json
|
||||||
```
|
```
|
||||||
|
|
@ -2484,7 +2484,7 @@ butonu, ekranın **özyinelemeli bağımlılık kapanışını** (`WizardExportC
|
||||||
```
|
```
|
||||||
wizard/{dosya}.json (ekranın kendisi + SubFormsDto[].Code ile bağlı
|
wizard/{dosya}.json (ekranın kendisi + SubFormsDto[].Code ile bağlı
|
||||||
alt formların wizard dosyaları, özyinelemeli)
|
alt formların wizard dosyaları, özyinelemeli)
|
||||||
data/App.DeveloperKit.CustomComponents.json (component + bağımlılıklarına süzülmüş kopya)
|
data/App.DeveloperKit.CustomComponents/{Id}.json (component + bağımlılıklarına süzülmüş kopya)
|
||||||
data/{ListFormCode}.json (ekranın SeedFilePath'i doluysa kendi veri aynası)
|
data/{ListFormCode}.json (ekranın SeedFilePath'i doluysa kendi veri aynası)
|
||||||
crud/{entity}.json (component'lerin/nesnenin kullandığı endpoint'ler)
|
crud/{entity}.json (component'lerin/nesnenin kullandığı endpoint'ler)
|
||||||
{sql|postgres}/{object|execute}/{nesne}.sql (SelectCommand'ın nesnesi + lookup sorgularının
|
{sql|postgres}/{object|execute}/{nesne}.sql (SelectCommand'ın nesnesi + lookup sorgularının
|
||||||
|
|
@ -3579,7 +3579,15 @@ kullanılacağı** `lowcode-reference.instructions.md` §12'dedir.
|
||||||
|
|
||||||
Ekrandan girilen kayıtların (widget tanımı, parametre tablosu, sabit liste) veritabanı
|
Ekrandan girilen kayıtların (widget tanımı, parametre tablosu, sabit liste) veritabanı
|
||||||
sıfırlandığında geri gelmesi için kullanılır. Bağlantı ListForm kaydındaki `SeedFilePath`
|
sıfırlandığında geri gelmesi için kullanılır. Bağlantı ListForm kaydındaki `SeedFilePath`
|
||||||
alanıdır; boşsa ne geri yükleme ne de senkronizasyon yapılır. Ekrandaki hangi işlemin dosyaya
|
alanıdır; boşsa ne geri yükleme ne de senkronizasyon yapılır.
|
||||||
|
|
||||||
|
**Hedef dosya ya da klasördür.** `SeedFilePath` `.json` ile bitiyorsa bütün satırlar tek
|
||||||
|
dosyadadır (varsayılan). Uzantısız yazılırsa hedef klasördür (`data/{ListFormCode}`) ve her satır
|
||||||
|
`{anahtar}.json` olarak kendi dosyasında durur; klasör yoksa ilk kayıtta oluşturulur. Klasör modu
|
||||||
|
satırı büyük ya da sık yazılan ekranlar içindir — şu an tek örnek Custom Components
|
||||||
|
(`SeedPathResolver.GetDataFolderPath`). Uygulayıcı `data/` altını özyinelemeli tarar ve bir
|
||||||
|
klasörün tamamını **tek unit of work'te**, ekran tanımını bir kez çözerek uygular; dosyadaki
|
||||||
|
`ListFormCode` boşsa ekran kodu klasör adından gelir. Ekrandaki hangi işlemin dosyaya
|
||||||
yansıyacağını `SeedSyncInsert`, `SeedSyncUpdate` ve `SeedSyncDelete` bayrakları belirler — üçü de
|
yansıyacağını `SeedSyncInsert`, `SeedSyncUpdate` ve `SeedSyncDelete` bayrakları belirler — üçü de
|
||||||
kapalıyken dosya yalnızca seed sırasında okunur, hiç yazılmaz.
|
kapalıyken dosya yalnızca seed sırasında okunur, hiç yazılmaz.
|
||||||
|
|
||||||
|
|
@ -3649,7 +3657,7 @@ Kurallar:
|
||||||
kur, `required` bayrağını bilinçli seç.
|
kur, `required` bayrağını bilinçli seç.
|
||||||
4. Görünürlük ve komut yetkilerini ver (`designerPermission`, `*Permission`).
|
4. Görünürlük ve komut yetkilerini ver (`designerPermission`, `*Permission`).
|
||||||
5. Wizard `ComponentKind: 1` ile menüye bağla.
|
5. Wizard `ComponentKind: 1` ile menüye bağla.
|
||||||
6. Doğrula: `data/App.DeveloperKit.CustomComponents.json` içindeki satır ve ilgili `crud/*.json` dosyaları oluştu mu.
|
6. Doğrula: `data/App.DeveloperKit.CustomComponents/{Id}.json` içindeki satır ve ilgili `crud/*.json` dosyaları oluştu mu.
|
||||||
|
|
||||||
### 10.4 "Kanban panosu"
|
### 10.4 "Kanban panosu"
|
||||||
|
|
||||||
|
|
@ -3695,7 +3703,7 @@ Bu tür isteklerin tamamı aynı iskeleti kullanır. Kullanıcı "avans talep fo
|
||||||
configs/seeds/host/
|
configs/seeds/host/
|
||||||
├── sql/object/Hr_T_AdvanceRequest.sql ← tablo (+ PostgreSQL hedefse postgres/object/…)
|
├── sql/object/Hr_T_AdvanceRequest.sql ← tablo (+ PostgreSQL hedefse postgres/object/…)
|
||||||
├── crud/Hr_T_AdvanceRequest.json ← yalnızca Custom Component yolunda gerekir
|
├── crud/Hr_T_AdvanceRequest.json ← yalnızca Custom Component yolunda gerekir
|
||||||
├── data/App.DeveloperKit.CustomComponents.json ← yalnızca serbest yerleşim isteniyorsa
|
├── data/App.DeveloperKit.CustomComponents/{Id}.json ← yalnızca serbest yerleşim isteniyorsa
|
||||||
├── wizard/AdvanceRequestApprovals.json ← (varsa) menüsüz alt ekran, küçük SeededAt
|
├── wizard/AdvanceRequestApprovals.json ← (varsa) menüsüz alt ekran, küçük SeededAt
|
||||||
└── wizard/AdvanceRequests.json ← ana ekran + menü + yetki + dil + onay akışı
|
└── wizard/AdvanceRequests.json ← ana ekran + menü + yetki + dil + onay akışı
|
||||||
```
|
```
|
||||||
|
|
|
||||||
25
README.md
25
README.md
|
|
@ -270,7 +270,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 `FinalSeeder` dosya adından türettiği procedure'ü çalıştırır (dosya adı = procedure adı, `schema.Procedure.sql` de olur). |
|
| `{sql\|postgres}/execute` | Nesneyi oluşturur; ayrıca tüm migration ve seed'ler bittikten sonra `FinalSeeder` 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. |
|
| `wizard` | ListForm Wizard'ın ürettiği `.json` konfigürasyonları; `WizardDataSeeder` kendi kapsamındaki dosyaları okur. |
|
||||||
| `crud` | CrudEndpoint konfigürasyonları; `CrudDataSeeder` kendi kapsamındaki dosyaları `ICrudEndpointSeedApplier` üzerinden uygular. Politika **damga karşılaştırmalıdır**: eksik endpoint her zaman eklenir, mevcut bir kayıt yalnızca dosyanın `GeneratedAt` damgası kaydın `LastModificationTime`/`CreationTime` değerinden **yeniyse** güncellenir ya da silinir; aksi halde kayıt korunur ve log'a drift uyarısı düşer. Böylece CRUD Endpoint Manager'dan yapılan bir revizyon depodan gelen eski bir dosya yüzünden migrate sırasında geri alınmaz. Damgası olmayan (eski biçimde yazılmış) dosya hiçbir zaman yeni sayılmaz. |
|
| `crud` | CrudEndpoint konfigürasyonları; `CrudDataSeeder` kendi kapsamındaki dosyaları `ICrudEndpointSeedApplier` üzerinden uygular. Politika **damga karşılaştırmalıdır**: eksik endpoint her zaman eklenir, mevcut bir kayıt yalnızca dosyanın `GeneratedAt` damgası kaydın `LastModificationTime`/`CreationTime` değerinden **yeniyse** güncellenir ya da silinir; aksi halde kayıt korunur ve log'a drift uyarısı düşer. Böylece CRUD Endpoint Manager'dan yapılan bir revizyon depodan gelen eski bir dosya yüzünden migrate sırasında geri alınmaz. Damgası olmayan (eski biçimde yazılmış) dosya hiçbir zaman yeni sayılmaz. |
|
||||||
| `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. |
|
| `data` | Ekranlardan girilen listelerin veri aynası: tek dosya (`{ListFormCode}.json`) ya da satır başına dosya tutan klasör (`{ListFormCode}/{anahtar}.json`; klasör özyinelemeli taranır ve tamamı tek unit of work'te uygulanır). `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,
|
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.
|
fakat `SqlDataSeeder` bunları uyarı ile loglar — ilgili klasöre taşınmaları beklenir.
|
||||||
|
|
@ -372,7 +372,7 @@ Ekran davranışı büyük ölçüde JSON kolonlarında saklanır. Öne çıkanl
|
||||||
| `ColumnOptionJson`, `PagerOptionJson`, `SelectionJson`, `StateStoringJson` | Sütun, sayfalama, seçim ve durum saklama ayarları. |
|
| `ColumnOptionJson`, `PagerOptionJson`, `SelectionJson`, `StateStoringJson` | Sütun, sayfalama, seçim ve durum saklama ayarları. |
|
||||||
| `Insert/Update/DeleteCommand` (+ `Before` / `After`) | CRUD komutları ve öncesi/sonrası çalışacak SQL kancaları. |
|
| `Insert/Update/DeleteCommand` (+ `Before` / `After`) | CRUD komutları ve öncesi/sonrası çalışacak SQL kancaları. |
|
||||||
| `Insert/Update/DeleteServiceAddress` | Varsayılan olarak `list-form-data/*`; özel endpoint'e yönlendirilebilir. |
|
| `Insert/Update/DeleteServiceAddress` | Varsayılan olarak `list-form-data/*`; özel endpoint'e yönlendirilebilir. |
|
||||||
| `SeedFilePath` | Doluysa seed sırasında listenin verisi kapsam klasöründeki bu JSON dosyasından geri yüklenir (`data/{ListFormCode}.json`). |
|
| `SeedFilePath` | Doluysa seed sırasında listenin verisi kapsam klasöründeki bu hedeften geri yüklenir: `.json` ile biten değer tek dosya (`data/{ListFormCode}.json`), uzantısız değer satır başına dosya tutan klasördür (`data/{ListFormCode}/{anahtar}.json`). |
|
||||||
| `SeedSyncInsert` / `SeedSyncUpdate` / `SeedSyncDelete` | Ekranda yapılan hangi işlemin seed dosyasına yansıyacağı; kapalıysa o işlem dosyaya dokunmaz. |
|
| `SeedSyncInsert` / `SeedSyncUpdate` / `SeedSyncDelete` | Ekranda yapılan hangi işlemin seed dosyasına yansıyacağı; kapalıysa o işlem dosyaya dokunmaz. |
|
||||||
| `SubFormsJson` | Ana-detay ilişkisi (ParentFieldName → ChildFieldName eşlemesi). |
|
| `SubFormsJson` | Ana-detay ilişkisi (ParentFieldName → ChildFieldName eşlemesi). |
|
||||||
| `WidgetsJson` | Ekranın üstünde gösterilecek KPI kartları. |
|
| `WidgetsJson` | Ekranın üstünde gösterilecek KPI kartları. |
|
||||||
|
|
@ -579,8 +579,8 @@ eklenir ve onların bağımlılıkları da aynı kurallarla toplanır. Böylece
|
||||||
| Yol | İçerik |
|
| Yol | İçerik |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `wizard/{dosya}.json` | Wizard tanımının kendisi **ve** `SubFormsDto[].Code` ile bağlı alt form ekranlarının wizard dosyaları (özyinelemeli) |
|
| `wizard/{dosya}.json` | Wizard tanımının kendisi **ve** `SubFormsDto[].Code` ile bağlı alt form ekranlarının wizard dosyaları (özyinelemeli) |
|
||||||
| `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ı |
|
| `data/App.DeveloperKit.CustomComponents/{Id}.json` | Custom yolunda bağlı component ve bağımlılıkları — yalnızca bu bileşenlerin satır dosyaları |
|
||||||
| `data/{ListFormCode}.json` | Ekranın `SeedFilePath` değeri doluysa kendi veri aynası |
|
| `data/{ListFormCode}.json` ya da `data/{ListFormCode}/{anahtar}.json` | Ekranın `SeedFilePath` değeri doluysa kendi veri aynası (dosya ya da klasör) |
|
||||||
| `crud/{entity}.json` | Component'lerin/nesnenin kullandığı CRUD endpoint tanımları |
|
| `crud/{entity}.json` | Component'lerin/nesnenin kullandığı CRUD endpoint tanımları |
|
||||||
| `{sql\|postgres}/{object\|execute}/{nesne}.sql` | `SelectCommand`'a karşılık gelen SQL nesnesi, lookup sorgularının (`LookupDto.LookupQuery`) `FROM`/`JOIN` ile işaret ettiği nesneler ve Custom yolunda CRUD endpoint'lerinin dayandığı tablolar |
|
| `{sql\|postgres}/{object\|execute}/{nesne}.sql` | `SelectCommand`'a karşılık gelen SQL nesnesi, lookup sorgularının (`LookupDto.LookupQuery`) `FROM`/`JOIN` ile işaret ettiği nesneler ve Custom yolunda CRUD endpoint'lerinin dayandığı tablolar |
|
||||||
|
|
||||||
|
|
@ -739,7 +739,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ı. |
|
| **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. |
|
| **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. |
|
| **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/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,.Export,.Import}`. |
|
| **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/{Id}.json` klasörüne senkronlanır. Yetkiler `App.DeveloperKit.CustomComponents{,.Create,.Update,.Delete,.Export,.Import}`. |
|
||||||
| **Visual Designer** | `/admin/developerkit/components/design/:id` | Custom Component listesindeki **Design** butonundan açılır; sürükle-bırak kanvas ile bileşen üretimi ve kod üretimi (`visualDesigner/codeGenerator.ts`). |
|
| **Visual Designer** | `/admin/developerkit/components/design/:id` | Custom Component listesindeki **Design** butonundan açılır; 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. |
|
| **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}`. |
|
| **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}`. |
|
||||||
|
|
@ -798,9 +798,10 @@ 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
|
tasarımcı dokümanı geri okunabilir. `sourceMode` alanı `visual` ya da `code` olur; kod moduna
|
||||||
geçilen bir bileşen kanvasa geri dönmez.
|
geçilen bir bileşen kanvasa geri dönmez.
|
||||||
|
|
||||||
**Seed senkronizasyonu.** Bileşenlerin seed kaynağı Custom Components ekranının veri dosyasıdır:
|
**Seed senkronizasyonu.** Bileşenlerin seed kaynağı Custom Components ekranının veri klasörüdür:
|
||||||
`configs/seeds/{host|tenants/{tenantId}}/data/App.DeveloperKit.CustomComponents.json`. Bileşene
|
`configs/seeds/{host|tenants/{tenantId}}/data/App.DeveloperKit.CustomComponents/{Id}.json` — her
|
||||||
yazan **iki yol** da bu dosyaya işlenir (`ListFormSeedDataSynchronizer`): listeden yapılan
|
bileşen kendi dosyasındadır; bileşen kodu satırın içinde durduğu için tek dosya hızla büyürdü.
|
||||||
|
Bileşene yazan **iki yol** da bu klasöre işlenir (`ListFormSeedDataSynchronizer`): listeden yapılan
|
||||||
ekleme/güncelleme/silme ve Visual Designer'ın kaydı (`CustomComponentAppService`). Veritabanı
|
ekleme/güncelleme/silme ve Visual Designer'ın kaydı (`CustomComponentAppService`). Veritabanı
|
||||||
sıfırlandığında `ListFormSeedDataApplier` satırları geri yazar; `Code`, `Props` ve `DataSources`
|
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ı
|
kolonları bu yüzden ekranda gizli alan olarak tanımlıdır. `DataSources` sütunu tasarımcı
|
||||||
|
|
@ -863,7 +864,7 @@ tıklamak seçim yapar, sayfayı değiştirmez — tıklama yakalama evresinde d
|
||||||
**Diğer notlar.** `PlatformIcon` tasarımcı dokümanındaki ikon adını çözer; `selectComponents.ts`
|
**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
|
`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
|
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/data/App.DeveloperKit.CustomComponents.json`
|
nokta budur. Yeni bileşen `configs/seeds/host/data/App.DeveloperKit.CustomComponents/{Id}.json`
|
||||||
içindeki bir satır olarak başlar.
|
içindeki bir satır olarak başlar.
|
||||||
|
|
||||||
**Dinamik servis yaşam döngüsü:** `DynamicServiceCompiler` kodu derler →
|
**Dinamik servis yaşam döngüsü:** `DynamicServiceCompiler` kodu derler →
|
||||||
|
|
@ -993,7 +994,7 @@ bileşen otomatik olarak public route olur ve `PublicLayout` altında yayınlan
|
||||||
DeveloperKit > Custom Components (Visual Designer) üzerinden değişir. Aynı yolu bir fiziksel route
|
DeveloperKit > Custom Components (Visual Designer) üzerinden değişir. Aynı yolu bir fiziksel route
|
||||||
ile paylaşan custom component kazanır; karşılığı olan dosyası bulunmayan rota kaydı ise hiç
|
ile paylaşan custom component kazanır; karşılığı olan dosyası bulunmayan rota kaydı ise hiç
|
||||||
yüklenmez (`hasPhysicalComponent`) — seeder yalnızca eklediği için eski kayıtlar veritabanında
|
yüklenmez (`hasPhysicalComponent`) — seeder yalnızca eklediği için eski kayıtlar veritabanında
|
||||||
kalsa bile ekran patlamaz. Seed: `configs/seeds/host/data/App.DeveloperKit.CustomComponents.json`;
|
kalsa bile ekran patlamaz. Seed: `configs/seeds/host/data/App.DeveloperKit.CustomComponents/{Id}.json`;
|
||||||
bileşenin ayrı dosyası yoktur, her bileşen bu dosyada bir satırdır.
|
bileşenin ayrı dosyası yoktur, her bileşen bu dosyada bir satırdır.
|
||||||
|
|
||||||
Runtime kapsamı sınırlıdır: bileşen kodundaki `import` satırları derlemede temizlenir,
|
Runtime kapsamı sınırlıdır: bileşen kodundaki `import` satırları derlemede temizlenir,
|
||||||
|
|
@ -1005,7 +1006,7 @@ aynı sözlük), gezinme `<a href>` ile yazılır. Kullanıcıya görünen metin
|
||||||
gelir.
|
gelir.
|
||||||
|
|
||||||
**Tailwind sınıf yüzeyi.** Bileşenin kodu veritabanında yaşar; derleme sırasında taranan tek
|
**Tailwind sınıf yüzeyi.** Bileşenin kodu veritabanında yaşar; derleme sırasında taranan tek
|
||||||
kaynak seed dosyasıdır (`configs/seeds/*/data/App.DeveloperKit.CustomComponents.json`). Bu dosya
|
kaynak seed dosyalarıdır (`configs/seeds/*/data/App.DeveloperKit.CustomComponents/{Id}.json`). Bu klasör
|
||||||
depo kökünde, `ui/` dışındadır — derleme bağlamı yalnızca `ui/` klasörü olduğunda (docker, ayrı
|
depo kökünde, `ui/` dışındadır — derleme bağlamı yalnızca `ui/` klasörü olduğunda (docker, ayrı
|
||||||
checkout) glob boş döner ve seed'e özgü sınıflar (`md:col-span-4`, `bg-gray-950/80`, `opacity-20`)
|
checkout) glob boş döner ve seed'e özgü sınıflar (`md:col-span-4`, `bg-gray-950/80`, `opacity-20`)
|
||||||
CSS'e hiç girmez: uygulama yerelde doğru, sunucuda stilsiz görünür. Bu yüzden aynı sınıflar var
|
CSS'e hiç girmez: uygulama yerelde doğru, sunucuda stilsiz görünür. Bu yüzden aynı sınıflar var
|
||||||
|
|
@ -1748,7 +1749,7 @@ configs/seeds/
|
||||||
├── host/ # host kapsamı
|
├── host/ # host kapsamı
|
||||||
│ ├── wizard/ # {Ad}.json → WizardDataSeeder
|
│ ├── wizard/ # {Ad}.json → WizardDataSeeder
|
||||||
│ ├── crud/ # {EntityName}.json → CrudEndpointSeedApplier
|
│ ├── crud/ # {EntityName}.json → CrudEndpointSeedApplier
|
||||||
│ ├── data/ # {ListFormCode}.json → ListFormSeedDataApplier
|
│ ├── data/ # {ListFormCode}.json ya da {ListFormCode}/{anahtar}.json → ListFormSeedDataApplier
|
||||||
│ │ # App.Menus.{Routes,Module,Menu}.json — route/modül/menü ağacı
|
│ │ # App.Menus.{Routes,Module,Menu}.json — route/modül/menü ağacı
|
||||||
│ │ # Abp.Identity.Permission{Groups,s}.json — yetki tanımları
|
│ │ # Abp.Identity.Permission{Groups,s}.json — yetki tanımları
|
||||||
│ │ # App.Definitions.{CountryGroup,Country,City,District}.json — adres zinciri
|
│ │ # App.Definitions.{CountryGroup,Country,City,District}.json — adres zinciri
|
||||||
|
|
|
||||||
|
|
@ -46,10 +46,15 @@ public enum SeedSyncOperation
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// <c>ListForm.SeedFilePath</c> dolu olan ekranlarin verisi kapsam klasorundeki bir JSON dosyasinda
|
/// <c>ListForm.SeedFilePath</c> dolu olan ekranlarin verisi kapsam klasorunde aynalanir; veritabani
|
||||||
/// aynalanir; veritabani sifirlandiginda <c>ListFormSeedDataApplier</c> ayni dosyadan geri yukler.
|
/// sifirlandiginda <c>ListFormSeedDataApplier</c> ayni yerden geri yukler.
|
||||||
/// Senkronizasyon satir bazlidir: yalnizca islem goren satir okunup dosyadaki karsiligina yazilir,
|
/// Senkronizasyon satir bazlidir: yalnizca islem goren satir okunup karsiligina yazilir,
|
||||||
/// boylece her kayitta listenin tamami sorgulanmaz ve dosyadaki satir sirasi korunur.
|
/// boylece her kayitta listenin tamami sorgulanmaz ve dosyadaki satir sirasi korunur.
|
||||||
|
/// <para>
|
||||||
|
/// Hedef iki bicimde olabilir: deger <c>.json</c> ile bitiyorsa butun satirlar tek dosyadadir,
|
||||||
|
/// aksi halde klasordur ve her satir <c>{anahtar}.json</c> olarak kendi dosyasinda durur. Klasor
|
||||||
|
/// modunda bir kayit yalnizca kendi dosyasina dokunur; tek dosya her kayitta bastan yazilir.
|
||||||
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ListFormSeedDataSynchronizer(
|
public class ListFormSeedDataSynchronizer(
|
||||||
IRepository<ListForm, Guid> listFormRepository,
|
IRepository<ListForm, Guid> listFormRepository,
|
||||||
|
|
@ -60,21 +65,33 @@ public class ListFormSeedDataSynchronizer(
|
||||||
{
|
{
|
||||||
private const int SyncAllPageSize = 10000;
|
private const int SyncAllPageSize = 10000;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ekranin seed hedefi. <see cref="IsFolder"/> true ise her satir klasorun altinda kendi
|
||||||
|
/// dosyasindadir (<c>{anahtar}.json</c>); false ise butun satirlar tek dosyadadir.
|
||||||
|
/// </summary>
|
||||||
|
private sealed record SeedTarget(string Path, bool IsFolder);
|
||||||
|
|
||||||
public async Task SyncRowAsync(string listFormCode, object key, SeedSyncOperation operation)
|
public async Task SyncRowAsync(string listFormCode, object key, SeedSyncOperation operation)
|
||||||
{
|
{
|
||||||
await ExecuteAsync(listFormCode, IsEnabled(operation), async (listForm, filePath) =>
|
await ExecuteAsync(listFormCode, IsEnabled(operation), async (listForm, target) =>
|
||||||
{
|
{
|
||||||
// Insert ucu her veri kaynaginda anahtari geri dondurmez; anahtarsiz kaldigimizda
|
// Insert ucu her veri kaynaginda anahtari geri dondurmez; anahtarsiz kaldigimizda
|
||||||
// ya da satir okunamadiginda dosya listenin tamamiyla yenilenir, bayat kalmaz.
|
// ya da satir okunamadiginda hedef listenin tamamiyla yenilenir, bayat kalmaz.
|
||||||
var row = key == null ? null : await ReadRowAsync(listForm, key);
|
var row = key == null ? null : await ReadRowAsync(listForm, key);
|
||||||
if (row == null)
|
if (row == null)
|
||||||
{
|
{
|
||||||
logger.LogInformation("Row '{Key}' could not be read for '{ListFormCode}', seed file is rewritten from the list.", key, listForm.ListFormCode);
|
logger.LogInformation("Row '{Key}' could not be read for '{ListFormCode}', seed file is rewritten from the list.", key, listForm.ListFormCode);
|
||||||
await WriteAllRowsAsync(listForm, filePath);
|
await WriteAllRowsAsync(listForm, target);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var seedFile = await ReadSeedFileAsync(filePath, listForm);
|
if (target.IsFolder)
|
||||||
|
{
|
||||||
|
await WriteRowFileAsync(listForm, target, key, row);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var seedFile = await ReadSeedFileAsync(target.Path, listForm);
|
||||||
var index = FindRowIndex(seedFile, listForm.KeyFieldName, key);
|
var index = FindRowIndex(seedFile, listForm.KeyFieldName, key);
|
||||||
|
|
||||||
if (index >= 0)
|
if (index >= 0)
|
||||||
|
|
@ -82,21 +99,39 @@ public class ListFormSeedDataSynchronizer(
|
||||||
else
|
else
|
||||||
seedFile.Rows.Add(row);
|
seedFile.Rows.Add(row);
|
||||||
|
|
||||||
await WriteSeedFileAsync(filePath, seedFile);
|
await WriteSeedFileAsync(target.Path, seedFile);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task RemoveRowsAsync(string listFormCode, object[] keys)
|
public async Task RemoveRowsAsync(string listFormCode, object[] keys)
|
||||||
{
|
{
|
||||||
await ExecuteAsync(listFormCode, listForm => listForm.SeedSyncDelete, async (listForm, filePath) =>
|
await ExecuteAsync(listFormCode, listForm => listForm.SeedSyncDelete, async (listForm, target) =>
|
||||||
{
|
{
|
||||||
if (keys == null || keys.Length == 0 || !File.Exists(filePath))
|
if (keys == null || keys.Length == 0)
|
||||||
{
|
{
|
||||||
await WriteAllRowsAsync(listForm, filePath);
|
await WriteAllRowsAsync(listForm, target);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var seedFile = await ReadSeedFileAsync(filePath, listForm);
|
if (target.IsFolder)
|
||||||
|
{
|
||||||
|
foreach (var key in keys)
|
||||||
|
{
|
||||||
|
var rowPath = ResolveRowFilePath(target, key);
|
||||||
|
if (rowPath != null && File.Exists(rowPath))
|
||||||
|
File.Delete(rowPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!File.Exists(target.Path))
|
||||||
|
{
|
||||||
|
await WriteAllRowsAsync(listForm, target);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var seedFile = await ReadSeedFileAsync(target.Path, listForm);
|
||||||
var removed = false;
|
var removed = false;
|
||||||
|
|
||||||
foreach (var key in keys)
|
foreach (var key in keys)
|
||||||
|
|
@ -110,7 +145,7 @@ public class ListFormSeedDataSynchronizer(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (removed)
|
if (removed)
|
||||||
await WriteSeedFileAsync(filePath, seedFile);
|
await WriteSeedFileAsync(target.Path, seedFile);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -126,8 +161,8 @@ public class ListFormSeedDataSynchronizer(
|
||||||
: listForm => listForm.SeedSyncUpdate;
|
: listForm => listForm.SeedSyncUpdate;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Dosyayi listenin tamamiyla yeniden yazar.</summary>
|
/// <summary>Hedefi listenin tamamiyla yeniden yazar.</summary>
|
||||||
private async Task WriteAllRowsAsync(ListForm listForm, string filePath)
|
private async Task WriteAllRowsAsync(ListForm listForm, SeedTarget target)
|
||||||
{
|
{
|
||||||
var result = await listFormSelectAppService.GetSelectAsync(new SelectRequestDto
|
var result = await listFormSelectAppService.GetSelectAsync(new SelectRequestDto
|
||||||
{
|
{
|
||||||
|
|
@ -138,13 +173,116 @@ public class ListFormSeedDataSynchronizer(
|
||||||
RequireGroupCount = false,
|
RequireGroupCount = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
List<Dictionary<string, object>> rows = ToRows((object)result?.Data);
|
||||||
|
|
||||||
|
if (target.IsFolder)
|
||||||
|
{
|
||||||
|
await WriteRowFilesAsync(listForm, target, rows);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Uygulama sirasi dosyanin kendi bilgisidir; listeden uretilemez, var olan degeri korunur.
|
// Uygulama sirasi dosyanin kendi bilgisidir; listeden uretilemez, var olan degeri korunur.
|
||||||
|
var seedFile = await ReadSeedFileAsync(target.Path, listForm);
|
||||||
|
seedFile.Rows = rows;
|
||||||
|
|
||||||
|
await WriteSeedFileAsync(target.Path, seedFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Klasor modunda her satiri kendi dosyasina yazar ve listede kalmayan satirlarin dosyalarini
|
||||||
|
/// siler — klasor listenin aynasidir.
|
||||||
|
/// <para>
|
||||||
|
/// Temizlik yalnizca liste tek sayfaya sigdiginda yapilir: sayfa sinirina dayanan bir listede
|
||||||
|
/// gorulmeyen satirlarin dosyasi silinirse veri kaybolur.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
private async Task WriteRowFilesAsync(ListForm listForm, SeedTarget target, List<Dictionary<string, object>> rows)
|
||||||
|
{
|
||||||
|
var order = await ReadFolderOrderAsync(target);
|
||||||
|
var written = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
foreach (var row in rows)
|
||||||
|
{
|
||||||
|
var fileName = SeedPathResolver.GetRowFileName(ToComparableText(row.GetValueOrDefault(listForm.KeyFieldName)));
|
||||||
|
if (fileName == null)
|
||||||
|
{
|
||||||
|
logger.LogWarning("A row without a key was skipped while writing the '{ListFormCode}' seed folder.", listForm.ListFormCode);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var filePath = Path.Combine(target.Path, fileName);
|
||||||
|
var seedFile = await ReadSeedFileAsync(filePath, listForm);
|
||||||
|
seedFile.Rows = [row];
|
||||||
|
seedFile.Order = order;
|
||||||
|
|
||||||
|
await WriteSeedFileAsync(filePath, seedFile);
|
||||||
|
written.Add(fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rows.Count >= SyncAllPageSize)
|
||||||
|
return;
|
||||||
|
|
||||||
|
foreach (var filePath in Directory.EnumerateFiles(target.Path, "*.json"))
|
||||||
|
{
|
||||||
|
if (!written.Contains(Path.GetFileName(filePath)))
|
||||||
|
File.Delete(filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Klasor modunda tek bir satirin dosyasini yazar.</summary>
|
||||||
|
private async Task WriteRowFileAsync(ListForm listForm, SeedTarget target, object key, Dictionary<string, object> row)
|
||||||
|
{
|
||||||
|
var filePath = ResolveRowFilePath(target, key);
|
||||||
|
if (filePath == null)
|
||||||
|
{
|
||||||
|
logger.LogWarning("Key '{Key}' cannot be used as a file name, the '{ListFormCode}' seed row was not written.", key, listForm.ListFormCode);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var isNew = !File.Exists(filePath);
|
||||||
|
|
||||||
var seedFile = await ReadSeedFileAsync(filePath, listForm);
|
var seedFile = await ReadSeedFileAsync(filePath, listForm);
|
||||||
seedFile.Rows = ToRows((object)result?.Data);
|
seedFile.Rows = [row];
|
||||||
|
|
||||||
|
// Yeni dosya uygulama sirasini kardeslerinden alir; 0 ile yazilsaydi ekran, bagimli oldugu
|
||||||
|
// tanimlardan once uygulanirdi.
|
||||||
|
if (isNew)
|
||||||
|
seedFile.Order = await ReadFolderOrderAsync(target);
|
||||||
|
|
||||||
await WriteSeedFileAsync(filePath, seedFile);
|
await WriteSeedFileAsync(filePath, seedFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Klasordeki satir dosyalarinin tasidigi uygulama sirasi (<c>Order</c>); bir ekranin butun satir
|
||||||
|
/// dosyalarinda ayni degerdir, bu yuzden ilk kardes dosyadan okunur.
|
||||||
|
/// </summary>
|
||||||
|
private static async Task<int> ReadFolderOrderAsync(SeedTarget target)
|
||||||
|
{
|
||||||
|
var sibling = Directory.EnumerateFiles(target.Path, "*.json").FirstOrDefault();
|
||||||
|
if (sibling == null)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var seedFile = JsonSerializer.Deserialize<ListFormSeedDataFileDto>(
|
||||||
|
await File.ReadAllTextAsync(sibling),
|
||||||
|
ListFormSeedDataFileDto.JsonOptions);
|
||||||
|
|
||||||
|
return seedFile?.Order ?? 0;
|
||||||
|
}
|
||||||
|
catch (JsonException)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? ResolveRowFilePath(SeedTarget target, object key)
|
||||||
|
{
|
||||||
|
var fileName = SeedPathResolver.GetRowFileName(ToComparableText(key));
|
||||||
|
|
||||||
|
return fileName == null ? null : Path.Combine(target.Path, fileName);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Seed dosyasi bagli degilse ya da islemin bayragi kapaliysa hicbir sey yapmaz.
|
/// Seed dosyasi bagli degilse ya da islemin bayragi kapaliysa hicbir sey yapmaz.
|
||||||
/// Senkronizasyon hatasi kullanicinin kayit islemini bozmaz; yalnizca loglanir.
|
/// Senkronizasyon hatasi kullanicinin kayit islemini bozmaz; yalnizca loglanir.
|
||||||
|
|
@ -152,7 +290,7 @@ public class ListFormSeedDataSynchronizer(
|
||||||
private async Task ExecuteAsync(
|
private async Task ExecuteAsync(
|
||||||
string listFormCode,
|
string listFormCode,
|
||||||
Func<ListForm, bool> isEnabled,
|
Func<ListForm, bool> isEnabled,
|
||||||
Func<ListForm, string, Task> action)
|
Func<ListForm, SeedTarget, Task> action)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(listFormCode))
|
if (string.IsNullOrWhiteSpace(listFormCode))
|
||||||
return;
|
return;
|
||||||
|
|
@ -163,14 +301,17 @@ public class ListFormSeedDataSynchronizer(
|
||||||
if (listForm == null || string.IsNullOrWhiteSpace(listForm.SeedFilePath) || !isEnabled(listForm))
|
if (listForm == null || string.IsNullOrWhiteSpace(listForm.SeedFilePath) || !isEnabled(listForm))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var filePath = SeedPathResolver.GetScopePath(
|
var isFolder = SeedPathResolver.IsSeedFolderPath(listForm.SeedFilePath);
|
||||||
|
|
||||||
|
var fullPath = SeedPathResolver.GetScopePath(
|
||||||
configuration,
|
configuration,
|
||||||
currentTenant.Id,
|
currentTenant.Id,
|
||||||
listForm.SeedFilePath.Replace('\\', '/').Split('/'));
|
listForm.SeedFilePath.Replace('\\', '/').Trim('/').Split('/'));
|
||||||
|
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(filePath)!);
|
// Klasor ilk kayitta olusur; ekran, hedefi diskte hazir bulmak zorunda degildir.
|
||||||
|
Directory.CreateDirectory(isFolder ? fullPath : Path.GetDirectoryName(fullPath)!);
|
||||||
|
|
||||||
await action(listForm, filePath);
|
await action(listForm, new SeedTarget(fullPath, isFolder));
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -2082,12 +2082,24 @@ public class ListFormWizardAppService(
|
||||||
var isScriptFolder = segments[0].Equals(SeedPathResolver.SqlFolder, StringComparison.OrdinalIgnoreCase)
|
var isScriptFolder = segments[0].Equals(SeedPathResolver.SqlFolder, StringComparison.OrdinalIgnoreCase)
|
||||||
|| segments[0].Equals(SeedPathResolver.PostgresFolder, StringComparison.OrdinalIgnoreCase);
|
|| segments[0].Equals(SeedPathResolver.PostgresFolder, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
var isDataFolder = segments[0].Equals(SeedPathResolver.DataFolder, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
// sql/postgres altinda object|execute klasoru zorunludur; digerlerinde alt klasor yoktur.
|
// sql/postgres altinda object|execute klasoru zorunludur; digerlerinde alt klasor yoktur.
|
||||||
if (isScriptFolder)
|
if (isScriptFolder)
|
||||||
{
|
{
|
||||||
if (segments.Length != 3 || !ImportAllowedScriptFolders.Contains(segments[1], StringComparer.OrdinalIgnoreCase))
|
if (segments.Length != 3 || !ImportAllowedScriptFolders.Contains(segments[1], StringComparer.OrdinalIgnoreCase))
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
else if (isDataFolder)
|
||||||
|
{
|
||||||
|
// Veri ya tek dosyadadir (data/{ListFormCode}.json) ya da satir basina dosya tutan bir
|
||||||
|
// klasorde (data/{ListFormCode}/{anahtar}.json); daha derin bir yol kural disidir.
|
||||||
|
if (segments.Length is not (2 or 3)
|
||||||
|
|| (segments.Length == 3 && segments[1].IndexOfAny(Path.GetInvalidFileNameChars()) >= 0))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
else if (segments.Length != 2)
|
else if (segments.Length != 2)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,9 @@ public sealed class WizardExportCollector(string scopeRoot)
|
||||||
/// <summary>ListFormCode -> wizard seed dosyasi; alt form cozumu icin bir kez kurulur.</summary>
|
/// <summary>ListFormCode -> wizard seed dosyasi; alt form cozumu icin bir kez kurulur.</summary>
|
||||||
private Dictionary<string, string>? wizardFilesByListFormCode;
|
private Dictionary<string, string>? wizardFilesByListFormCode;
|
||||||
|
|
||||||
|
/// <summary>Custom component seed dosyalari (kapsam-goreli yol + icerik); bir kez okunur.</summary>
|
||||||
|
private List<(string Path, ListFormSeedDataFileDto File)>? customComponentFiles;
|
||||||
|
|
||||||
public IReadOnlyList<string> Warnings => warnings;
|
public IReadOnlyList<string> Warnings => warnings;
|
||||||
|
|
||||||
/// <summary>Verilen wizard seed dosyasinin tasinabilir kapanisini toplar.</summary>
|
/// <summary>Verilen wizard seed dosyasinin tasinabilir kapanisini toplar.</summary>
|
||||||
|
|
@ -121,7 +124,7 @@ public sealed class WizardExportCollector(string scopeRoot)
|
||||||
|
|
||||||
// Ekranin verisi seed dosyasina aynalaniyorsa arsiv onsuz eksiktir.
|
// Ekranin verisi seed dosyasina aynalaniyorsa arsiv onsuz eksiktir.
|
||||||
var seedFilePath = listForm?.SeedFilePath;
|
var seedFilePath = listForm?.SeedFilePath;
|
||||||
if (!string.IsNullOrWhiteSpace(seedFilePath) && !AddDiskFile(seedFilePath, $"{screen} verisi"))
|
if (!string.IsNullOrWhiteSpace(seedFilePath) && !AddSeedData(seedFilePath, $"{screen} verisi"))
|
||||||
warnings.Add($"{seedFilePath}: ekranın veri seed dosyası bulunamadı.");
|
warnings.Add($"{seedFilePath}: ekranın veri seed dosyası bulunamadı.");
|
||||||
|
|
||||||
foreach (var field in seed.Fields ?? [])
|
foreach (var field in seed.Fields ?? [])
|
||||||
|
|
@ -249,17 +252,30 @@ public sealed class WizardExportCollector(string scopeRoot)
|
||||||
if (customComponents.Count == 0)
|
if (customComponents.Count == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var seedFile = await ReadCustomComponentFileAsync();
|
foreach (var (relativePath, seedFile) in await ReadCustomComponentFilesAsync())
|
||||||
if (seedFile == null)
|
{
|
||||||
return;
|
// Dosyadaki satir sirasi korunur; hedefte satirlar bu sirayla birlestirilir.
|
||||||
|
var rows = seedFile.Rows
|
||||||
|
.Where(x => customComponents.Contains(GetRowText(x, CustomComponentNameField) ?? string.Empty))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
// Dosyadaki satir sirasi korunur; hedefte satirlar bu sirayla birlestirilir.
|
// Satir basina dosya tutan klasorde ilgisiz bilesenin dosyasi arsive hic girmez.
|
||||||
seedFile.Rows = [.. seedFile.Rows.Where(x => customComponents.Contains(GetRowText(x, CustomComponentNameField) ?? string.Empty))];
|
if (rows.Count == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
var relativePath = SeedPathResolver.GetDataFilePath(CustomComponentsListFormCode);
|
var filtered = new ListFormSeedDataFileDto
|
||||||
var content = JsonSerializer.Serialize(seedFile, ListFormSeedDataFileDto.JsonOptions);
|
{
|
||||||
|
ListFormCode = seedFile.ListFormCode,
|
||||||
|
KeyFieldName = seedFile.KeyFieldName,
|
||||||
|
GeneratedAt = seedFile.GeneratedAt,
|
||||||
|
Order = seedFile.Order,
|
||||||
|
Rows = rows,
|
||||||
|
};
|
||||||
|
|
||||||
items[relativePath] = new WizardExportItem(relativePath, "Custom component tanımı", null, content);
|
var content = JsonSerializer.Serialize(filtered, ListFormSeedDataFileDto.JsonOptions);
|
||||||
|
|
||||||
|
items[relativePath] = new WizardExportItem(relativePath, "Custom component tanımı", null, content);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -283,12 +299,12 @@ public sealed class WizardExportCollector(string scopeRoot)
|
||||||
|
|
||||||
private async Task<Dictionary<string, Dictionary<string, object>>?> ReadCustomComponentRowsAsync()
|
private async Task<Dictionary<string, Dictionary<string, object>>?> ReadCustomComponentRowsAsync()
|
||||||
{
|
{
|
||||||
var seedFile = await ReadCustomComponentFileAsync();
|
var files = await ReadCustomComponentFilesAsync();
|
||||||
if (seedFile == null)
|
if (files.Count == 0)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
var rows = new Dictionary<string, Dictionary<string, object>>(StringComparer.OrdinalIgnoreCase);
|
var rows = new Dictionary<string, Dictionary<string, object>>(StringComparer.OrdinalIgnoreCase);
|
||||||
foreach (var row in seedFile.Rows)
|
foreach (var row in files.SelectMany(x => x.File.Rows))
|
||||||
{
|
{
|
||||||
var name = GetRowText(row, CustomComponentNameField);
|
var name = GetRowText(row, CustomComponentNameField);
|
||||||
if (!string.IsNullOrWhiteSpace(name))
|
if (!string.IsNullOrWhiteSpace(name))
|
||||||
|
|
@ -298,25 +314,45 @@ public sealed class WizardExportCollector(string scopeRoot)
|
||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<ListFormSeedDataFileDto?> ReadCustomComponentFileAsync()
|
/// <summary>
|
||||||
|
/// Custom component seed kaynagi. Ekranin <c>SeedFilePath</c> degeri burada bilinmedigi icin
|
||||||
|
/// kaynak diskten cozulur: satir basina dosya tutan klasor varsa oradaki dosyalar, yoksa tek
|
||||||
|
/// dosya okunur.
|
||||||
|
/// </summary>
|
||||||
|
private async Task<List<(string Path, ListFormSeedDataFileDto File)>> ReadCustomComponentFilesAsync()
|
||||||
{
|
{
|
||||||
var relativePath = SeedPathResolver.GetDataFilePath(CustomComponentsListFormCode);
|
if (customComponentFiles != null)
|
||||||
var sourcePath = ResolveScopeFilePath(relativePath);
|
return customComponentFiles;
|
||||||
if (sourcePath == null)
|
|
||||||
|
customComponentFiles = [];
|
||||||
|
|
||||||
|
var folderPath = SeedPathResolver.GetDataFolderPath(CustomComponentsListFormCode);
|
||||||
|
var relativePaths = ResolveScopeFolderPath(folderPath) == null
|
||||||
|
? [SeedPathResolver.GetDataFilePath(CustomComponentsListFormCode)]
|
||||||
|
: EnumerateSeedFolderFiles(folderPath).ToList();
|
||||||
|
|
||||||
|
foreach (var relativePath in relativePaths)
|
||||||
{
|
{
|
||||||
warnings.Add($"{relativePath}: custom component seed dosyası bulunamadı.");
|
var sourcePath = ResolveScopeFilePath(relativePath);
|
||||||
return null;
|
if (sourcePath == null)
|
||||||
|
{
|
||||||
|
warnings.Add($"{relativePath}: custom component seed dosyası bulunamadı.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var file = JsonSerializer.Deserialize<ListFormSeedDataFileDto>(await File.ReadAllTextAsync(sourcePath), JsonReadOptions);
|
||||||
|
if (file != null)
|
||||||
|
customComponentFiles.Add((relativePath, file));
|
||||||
|
}
|
||||||
|
catch (JsonException)
|
||||||
|
{
|
||||||
|
warnings.Add($"{relativePath}: custom component seed dosyası çözülemedi.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
return customComponentFiles;
|
||||||
{
|
|
||||||
return JsonSerializer.Deserialize<ListFormSeedDataFileDto>(await File.ReadAllTextAsync(sourcePath), JsonReadOptions);
|
|
||||||
}
|
|
||||||
catch (JsonException)
|
|
||||||
{
|
|
||||||
warnings.Add($"{relativePath}: custom component seed dosyası çözülemedi.");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Dosya toplama ──────────────────────────────────────────────────────────────
|
// ── Dosya toplama ──────────────────────────────────────────────────────────────
|
||||||
|
|
@ -386,6 +422,38 @@ public sealed class WizardExportCollector(string scopeRoot)
|
||||||
return Directory.Exists(path) ? Directory.EnumerateDirectories(path) : [];
|
return Directory.Exists(path) ? Directory.EnumerateDirectories(path) : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ekranin veri seed hedefini arsive ekler. Hedef klasorse (satir basina dosya) klasordeki butun
|
||||||
|
/// satir dosyalari girer; tek dosyaysa o dosya eklenir. Hicbir sey eklenemezse false doner.
|
||||||
|
/// </summary>
|
||||||
|
private bool AddSeedData(string? relativePath, string reason)
|
||||||
|
{
|
||||||
|
if (!SeedPathResolver.IsSeedFolderPath(relativePath))
|
||||||
|
return AddDiskFile(relativePath, reason);
|
||||||
|
|
||||||
|
var added = false;
|
||||||
|
|
||||||
|
foreach (var rowPath in EnumerateSeedFolderFiles(relativePath!))
|
||||||
|
added |= AddDiskFile(rowPath, reason);
|
||||||
|
|
||||||
|
return added;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Kapsam-goreli bir seed klasorundeki satir dosyalarinin kapsam-goreli yollari.</summary>
|
||||||
|
private IEnumerable<string> EnumerateSeedFolderFiles(string relativeFolderPath)
|
||||||
|
{
|
||||||
|
var normalized = relativeFolderPath.Replace('\\', '/').Trim('/');
|
||||||
|
var fullPath = ResolveScopeFolderPath(normalized);
|
||||||
|
|
||||||
|
if (fullPath == null)
|
||||||
|
return [];
|
||||||
|
|
||||||
|
return Directory
|
||||||
|
.EnumerateFiles(fullPath, "*.json")
|
||||||
|
.Select(x => $"{normalized}/{System.IO.Path.GetFileName(x)}")
|
||||||
|
.OrderBy(x => x, StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Kapsam icindeki var olan bir dosyayi arsive ekler; kapsam disina cikan ya da bulunmayan yol icin false doner.</summary>
|
/// <summary>Kapsam icindeki var olan bir dosyayi arsive ekler; kapsam disina cikan ya da bulunmayan yol icin false doner.</summary>
|
||||||
private bool AddDiskFile(string? relativePath, string reason)
|
private bool AddDiskFile(string? relativePath, string reason)
|
||||||
{
|
{
|
||||||
|
|
@ -418,6 +486,21 @@ public sealed class WizardExportCollector(string scopeRoot)
|
||||||
return File.Exists(fullPath) ? fullPath : null;
|
return File.Exists(fullPath) ? fullPath : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Kapsam icindeki var olan bir klasorun tam yolu; kapsam disina cikan yol icin null.</summary>
|
||||||
|
private string? ResolveScopeFolderPath(string? relativePath)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(relativePath) || relativePath.Contains("..", StringComparison.Ordinal))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var fullPath = System.IO.Path.GetFullPath(
|
||||||
|
System.IO.Path.Combine(scopeRoot, relativePath.Replace('/', System.IO.Path.DirectorySeparatorChar)));
|
||||||
|
|
||||||
|
if (!fullPath.StartsWith(scopeRoot + System.IO.Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return Directory.Exists(fullPath) ? fullPath : null;
|
||||||
|
}
|
||||||
|
|
||||||
private string? ResolveWizardFileByListFormCode(string listFormCode)
|
private string? ResolveWizardFileByListFormCode(string listFormCode)
|
||||||
{
|
{
|
||||||
wizardFilesByListFormCode ??= BuildWizardIndex(scopeRoot);
|
wizardFilesByListFormCode ??= BuildWizardIndex(scopeRoot);
|
||||||
|
|
|
||||||
|
|
@ -207,8 +207,8 @@ public sealed class WizardImportValidator(
|
||||||
|
|
||||||
var seedFilePath = seed.ListForm.SeedFilePath;
|
var seedFilePath = seed.ListForm.SeedFilePath;
|
||||||
if (!string.IsNullOrWhiteSpace(seedFilePath)
|
if (!string.IsNullOrWhiteSpace(seedFilePath)
|
||||||
&& !paths.Contains(seedFilePath)
|
&& !SeedDataIncoming(seedFilePath, paths)
|
||||||
&& !ScopeFileExists(seedFilePath))
|
&& !ScopeSeedDataExists(seedFilePath))
|
||||||
{
|
{
|
||||||
Add(WizardImportIssueSeverityEnum.Warning, path,
|
Add(WizardImportIssueSeverityEnum.Warning, path,
|
||||||
$"{screen}: ekranın veri dosyası ({seedFilePath}) ne zip'te ne hedefte var; ekran boş gelecek.");
|
$"{screen}: ekranın veri dosyası ({seedFilePath}) ne zip'te ne hedefte var; ekran boş gelecek.");
|
||||||
|
|
@ -248,9 +248,7 @@ public sealed class WizardImportValidator(
|
||||||
/// <summary>Veri dosyasi, satirlarin yazilacagi ekran tanimi olmadan uygulanamaz.</summary>
|
/// <summary>Veri dosyasi, satirlarin yazilacagi ekran tanimi olmadan uygulanamaz.</summary>
|
||||||
private async Task ValidateDataAsync(string path, ListFormSeedDataFileDto file, HashSet<string> paths)
|
private async Task ValidateDataAsync(string path, ListFormSeedDataFileDto file, HashSet<string> paths)
|
||||||
{
|
{
|
||||||
var listFormCode = string.IsNullOrWhiteSpace(file.ListFormCode)
|
var listFormCode = ResolveDataListFormCode(path, file);
|
||||||
? System.IO.Path.GetFileNameWithoutExtension(path)
|
|
||||||
: file.ListFormCode.Trim();
|
|
||||||
|
|
||||||
if (!incomingListFormCodes.Contains(listFormCode) && !await listFormExistsAsync(listFormCode))
|
if (!incomingListFormCodes.Contains(listFormCode) && !await listFormExistsAsync(listFormCode))
|
||||||
{
|
{
|
||||||
|
|
@ -405,12 +403,28 @@ public sealed class WizardImportValidator(
|
||||||
/// <summary>Veri dosyasinin custom component seed kaynagi olup olmadigi.</summary>
|
/// <summary>Veri dosyasinin custom component seed kaynagi olup olmadigi.</summary>
|
||||||
private static bool IsCustomComponentsFile(string path, ListFormSeedDataFileDto file)
|
private static bool IsCustomComponentsFile(string path, ListFormSeedDataFileDto file)
|
||||||
{
|
{
|
||||||
var listFormCode = string.IsNullOrWhiteSpace(file.ListFormCode)
|
|
||||||
? System.IO.Path.GetFileNameWithoutExtension(path)
|
|
||||||
: file.ListFormCode.Trim();
|
|
||||||
|
|
||||||
return string.Equals(
|
return string.Equals(
|
||||||
listFormCode, WizardExportCollector.CustomComponentsListFormCode, StringComparison.OrdinalIgnoreCase);
|
ResolveDataListFormCode(path, file),
|
||||||
|
WizardExportCollector.CustomComponentsListFormCode,
|
||||||
|
StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Veri dosyasinin ait oldugu ekran. Dosyada yaziliysa oradan gelir; degilse yoldan cikarilir:
|
||||||
|
/// satir klasorunde (<c>data/{ListFormCode}/{anahtar}.json</c>) klasorun adi, tek dosyada
|
||||||
|
/// uzantisiz dosya adi ekran kodudur.
|
||||||
|
/// </summary>
|
||||||
|
private static string ResolveDataListFormCode(string path, ListFormSeedDataFileDto file)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(file.ListFormCode))
|
||||||
|
return file.ListFormCode.Trim();
|
||||||
|
|
||||||
|
var segments = path.Replace('\\', '/').Trim('/').Split('/');
|
||||||
|
|
||||||
|
// data/{ListFormCode}/{anahtar}.json — ekran kodu klasor adidir.
|
||||||
|
return segments.Length > 2
|
||||||
|
? segments[^2]
|
||||||
|
: System.IO.Path.GetFileNameWithoutExtension(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Yardimcilar ────────────────────────────────────────────────────────────────
|
// ── Yardimcilar ────────────────────────────────────────────────────────────────
|
||||||
|
|
@ -471,30 +485,48 @@ public sealed class WizardImportValidator(
|
||||||
return targetCustomComponents;
|
return targetCustomComponents;
|
||||||
|
|
||||||
var names = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
var names = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
var relativePath = SeedPathResolver.GetDataFilePath(WizardExportCollector.CustomComponentsListFormCode);
|
|
||||||
var fullPath = ResolveScopeFilePath(relativePath);
|
|
||||||
|
|
||||||
if (fullPath == null)
|
foreach (var fullPath in ReadTargetCustomComponentFiles())
|
||||||
return targetCustomComponents = names;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
var file = JsonSerializer.Deserialize<ListFormSeedDataFileDto>(File.ReadAllText(fullPath), JsonReadOptions);
|
try
|
||||||
foreach (var row in file?.Rows ?? [])
|
|
||||||
{
|
{
|
||||||
var name = WizardExportCollector.GetRowText(row, WizardExportCollector.CustomComponentNameField);
|
var file = JsonSerializer.Deserialize<ListFormSeedDataFileDto>(File.ReadAllText(fullPath), JsonReadOptions);
|
||||||
if (!string.IsNullOrWhiteSpace(name))
|
foreach (var row in file?.Rows ?? [])
|
||||||
names.Add(name);
|
{
|
||||||
|
var name = WizardExportCollector.GetRowText(row, WizardExportCollector.CustomComponentNameField);
|
||||||
|
if (!string.IsNullOrWhiteSpace(name))
|
||||||
|
names.Add(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (JsonException)
|
||||||
|
{
|
||||||
|
// Hedef dosya bozuksa import bunu duzeltmek icin yapiliyor olabilir; kontrol atlanir.
|
||||||
}
|
}
|
||||||
}
|
|
||||||
catch (JsonException)
|
|
||||||
{
|
|
||||||
// Hedef dosya bozuksa import bunu duzeltmek icin yapiliyor olabilir; kontrol atlanir.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return targetCustomComponents = names;
|
return targetCustomComponents = names;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Hedefteki custom component seed dosyalari: satir basina dosya tutan klasor varsa onun
|
||||||
|
/// icindekiler, yoksa tek dosya.
|
||||||
|
/// </summary>
|
||||||
|
private List<string> ReadTargetCustomComponentFiles()
|
||||||
|
{
|
||||||
|
var folderPath = System.IO.Path.Combine(
|
||||||
|
scopeRoot,
|
||||||
|
SeedPathResolver.GetDataFolderPath(WizardExportCollector.CustomComponentsListFormCode)
|
||||||
|
.Replace('/', System.IO.Path.DirectorySeparatorChar));
|
||||||
|
|
||||||
|
if (Directory.Exists(folderPath))
|
||||||
|
return [.. Directory.EnumerateFiles(folderPath, "*.json")];
|
||||||
|
|
||||||
|
var filePath = ResolveScopeFilePath(
|
||||||
|
SeedPathResolver.GetDataFilePath(WizardExportCollector.CustomComponentsListFormCode));
|
||||||
|
|
||||||
|
return filePath == null ? [] : [filePath];
|
||||||
|
}
|
||||||
|
|
||||||
private SubFormDto[] ReadSubForms(WizardSeedFileDto seed, string path)
|
private SubFormDto[] ReadSubForms(WizardSeedFileDto seed, string path)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|
@ -547,6 +579,36 @@ public sealed class WizardImportValidator(
|
||||||
|
|
||||||
private bool ScopeFileExists(string? relativePath) => ResolveScopeFilePath(relativePath) != null;
|
private bool ScopeFileExists(string? relativePath) => ResolveScopeFilePath(relativePath) != null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ekranin veri seed hedefi hedef ortamda var mi. Hedef klasorse (satir basina dosya) klasorun
|
||||||
|
/// kendisine bakilir; tek dosyaysa dosyaya.
|
||||||
|
/// </summary>
|
||||||
|
private bool ScopeSeedDataExists(string? relativePath)
|
||||||
|
{
|
||||||
|
if (!SeedPathResolver.IsSeedFolderPath(relativePath))
|
||||||
|
return ScopeFileExists(relativePath);
|
||||||
|
|
||||||
|
var normalized = relativePath!.Replace('\\', '/').Trim('/');
|
||||||
|
if (normalized.Contains("..", StringComparison.Ordinal))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var fullPath = System.IO.Path.GetFullPath(
|
||||||
|
System.IO.Path.Combine(scopeRoot, normalized.Replace('/', System.IO.Path.DirectorySeparatorChar)));
|
||||||
|
|
||||||
|
return fullPath.StartsWith(scopeRoot + System.IO.Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& Directory.Exists(fullPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Ekranin veri seed hedefi zip'te var mi; klasor modunda klasorun altindaki satir dosyalari aranir.</summary>
|
||||||
|
private static bool SeedDataIncoming(string relativePath, HashSet<string> paths)
|
||||||
|
{
|
||||||
|
var normalized = relativePath.Replace('\\', '/').Trim('/');
|
||||||
|
|
||||||
|
return SeedPathResolver.IsSeedFolderPath(normalized)
|
||||||
|
? paths.Any(x => x.StartsWith(normalized + "/", StringComparison.OrdinalIgnoreCase))
|
||||||
|
: paths.Contains(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
private string? ResolveScopeFilePath(string? relativePath)
|
private string? ResolveScopeFilePath(string? relativePath)
|
||||||
{
|
{
|
||||||
var normalized = relativePath?.Replace('\\', '/').Trim('/');
|
var normalized = relativePath?.Replace('\\', '/').Trim('/');
|
||||||
|
|
|
||||||
|
|
@ -4965,8 +4965,8 @@
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
"key": "App.ListFormEdit.SeedFilePathHint",
|
"key": "App.ListFormEdit.SeedFilePathHint",
|
||||||
"en": "Relative to the scope folder, e.g. data/{ListFormCode}.json. When filled, insert/update/delete operations are mirrored to this file.",
|
"en": "Relative to the scope folder. A file (data/{ListFormCode}.json) keeps every row in one file; a folder (data/{ListFormCode}) keeps each row in its own {key}.json file. When filled, insert/update/delete operations are mirrored to the target.",
|
||||||
"tr": "Kapsam klasörüne göre görelidir, örneğin data/{ListFormCode}.json. Dolu ise ekleme/güncelleme/silme işlemleri bu dosyaya yansıtılır."
|
"tr": "Kapsam klasörüne göre görelidir. Dosya (data/{ListFormCode}.json) bütün satırları tek dosyada tutar; klasör (data/{ListFormCode}) her satırı kendi {anahtar}.json dosyasında tutar. Dolu ise ekleme/güncelleme/silme işlemleri bu hedefe yansıtılır."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
|
|
|
||||||
|
|
@ -7640,7 +7640,9 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
|
||||||
Description = $"{listFormName}.Description",
|
Description = $"{listFormName}.Description",
|
||||||
SelectCommandType = SelectCommandTypeEnum.Table,
|
SelectCommandType = SelectCommandTypeEnum.Table,
|
||||||
SelectCommand = customComponentTable,
|
SelectCommand = customComponentTable,
|
||||||
SeedFilePath = SeedPathResolver.GetDataFilePath(listFormName),
|
// Bilesen kodu satirin icinde durur; tek dosya hizla buyudugu icin her bilesen
|
||||||
|
// klasorun altinda kendi dosyasindadir.
|
||||||
|
SeedFilePath = SeedPathResolver.GetDataFolderPath(listFormName),
|
||||||
SeedSyncInsert = true,
|
SeedSyncInsert = true,
|
||||||
SeedSyncUpdate = true,
|
SeedSyncUpdate = true,
|
||||||
SeedSyncDelete = true,
|
SeedSyncDelete = true,
|
||||||
|
|
|
||||||
|
|
@ -125,11 +125,11 @@ public class ListFormSeedDataApplier(
|
||||||
|
|
||||||
using (currentTenant.Change(tenantId))
|
using (currentTenant.Change(tenantId))
|
||||||
{
|
{
|
||||||
foreach (var filePath in OrderFiles([.. filePaths]))
|
foreach (var target in BuildTargets(scopeRoot, filePaths))
|
||||||
{
|
{
|
||||||
// Her dosya kendi unit of work'unde islenir; ApplyAsync ile ayni kural.
|
// Her hedef kendi unit of work'unde islenir; ApplyAsync ile ayni kural.
|
||||||
using var uow = unitOfWorkManager.Begin(requiresNew: true, isTransactional: false);
|
using var uow = unitOfWorkManager.Begin(requiresNew: true, isTransactional: false);
|
||||||
results.Add(await ApplyFileAsync(filePath, overwriteExisting));
|
results.Add(await ApplyTargetAsync(target, overwriteExisting));
|
||||||
await uow.CompleteAsync();
|
await uow.CompleteAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -148,7 +148,11 @@ public class ListFormSeedDataApplier(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var jsonFiles = OrderFiles(Directory.GetFiles(dataPath, "*.json"));
|
await MigrateSeedFilePathsAsync(tenantId, dataPath);
|
||||||
|
|
||||||
|
// Alt klasorler de taranir: satir basina dosya tutan ekranin verisi
|
||||||
|
// data/{ListFormCode}/{anahtar}.json altindadir.
|
||||||
|
var jsonFiles = Directory.GetFiles(dataPath, "*.json", SearchOption.AllDirectories);
|
||||||
if (jsonFiles.Length == 0)
|
if (jsonFiles.Length == 0)
|
||||||
{
|
{
|
||||||
logger.LogInformation("No JSON files found in Seeds/{Scope}/data directory, skipping ListFormSeedDataApplier.", scopeFolderName);
|
logger.LogInformation("No JSON files found in Seeds/{Scope}/data directory, skipping ListFormSeedDataApplier.", scopeFolderName);
|
||||||
|
|
@ -157,14 +161,16 @@ public class ListFormSeedDataApplier(
|
||||||
|
|
||||||
logger.LogInformation("ListFormSeedDataApplier started for {Scope}. {Count} file(s) to be processed.", scopeFolderName, jsonFiles.Length);
|
logger.LogInformation("ListFormSeedDataApplier started for {Scope}. {Count} file(s) to be processed.", scopeFolderName, jsonFiles.Length);
|
||||||
|
|
||||||
|
var scopeRoot = Path.GetFullPath(SeedPathResolver.GetScopePath(configuration, tenantId));
|
||||||
|
|
||||||
using (currentTenant.Change(tenantId))
|
using (currentTenant.Change(tenantId))
|
||||||
{
|
{
|
||||||
foreach (var filePath in jsonFiles)
|
foreach (var target in BuildTargets(scopeRoot, jsonFiles))
|
||||||
{
|
{
|
||||||
// Seed contributor'larin unit of work'u kapandigi icin her dosya kendi
|
// Seed contributor'larin unit of work'u kapandigi icin her hedef kendi
|
||||||
// unit of work'unde islenir; aksi halde DbContext disposed gelir.
|
// unit of work'unde islenir; aksi halde DbContext disposed gelir.
|
||||||
using var uow = unitOfWorkManager.Begin(requiresNew: true, isTransactional: false);
|
using var uow = unitOfWorkManager.Begin(requiresNew: true, isTransactional: false);
|
||||||
await ApplyFileAsync(filePath, overwriteExisting: false);
|
await ApplyTargetAsync(target, overwriteExisting: false);
|
||||||
await uow.CompleteAsync();
|
await uow.CompleteAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -173,19 +179,77 @@ public class ListFormSeedDataApplier(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Dosyalari <c>Order</c> alanina, esitlikte dosya adina gore siralar. Yabanci anahtarla
|
/// Dosyalari uygulama hedeflerine ayirir ve <c>Order</c> alanina, esitlikte yola gore siralar.
|
||||||
/// bagli tablolarda referans verilen tanim once uygulanmalidir; okunamayan dosya sona kalir
|
/// Tek dosyaya bagli ekran bir hedeftir; satir basina dosya tutan klasorde ise klasorun tamami
|
||||||
/// ve hatasi kendi isleminde raporlanir.
|
/// tek hedeftir — boylece klasordeki butun satirlar tek unit of work'te, ekran tanimi bir kez
|
||||||
|
/// cozulerek uygulanir. Yabanci anahtarla bagli tablolarda referans verilen tanim once
|
||||||
|
/// uygulanmalidir; okunamayan dosya sona kalir ve hatasi kendi isleminde raporlanir.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static string[] OrderFiles(string[] filePaths)
|
private static SeedDataTarget[] BuildTargets(string scopeRoot, IEnumerable<string> filePaths)
|
||||||
{
|
{
|
||||||
|
var dataPath = Path.Combine(scopeRoot, SeedPathResolver.DataFolder);
|
||||||
|
|
||||||
return [.. filePaths
|
return [.. filePaths
|
||||||
.Select(filePath => (FilePath: filePath, Order: ReadOrder(filePath)))
|
.GroupBy(
|
||||||
|
filePath => IsRowFile(dataPath, filePath) ? Path.GetDirectoryName(filePath)! : filePath,
|
||||||
|
StringComparer.OrdinalIgnoreCase)
|
||||||
|
.Select(group => new SeedDataTarget(
|
||||||
|
Path.GetRelativePath(scopeRoot, group.Key).Replace('\\', '/'),
|
||||||
|
[.. group.OrderBy(a => a, StringComparer.OrdinalIgnoreCase)]))
|
||||||
|
.Select(target => (Target: target, Order: ReadOrder(target.FilePaths[0])))
|
||||||
.OrderBy(a => a.Order)
|
.OrderBy(a => a.Order)
|
||||||
.ThenBy(a => Path.GetFileName(a.FilePath))
|
.ThenBy(a => a.Target.Path, StringComparer.OrdinalIgnoreCase)
|
||||||
.Select(a => a.FilePath)];
|
.Select(a => a.Target)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tek dosyaya bagli bir ekranin seed hedefini, kapsamda o ekranin satir klasoru varsa klasore
|
||||||
|
/// tasir. Bir ekran klasor moduna gecirildiginde var olan kurulumlarin kaydi artik uretilmeyen
|
||||||
|
/// eski dosyayi gosterir; klasorun diskte olmasi gecisin tek isaretidir. Elle verilmis bir yola
|
||||||
|
/// dokunulmaz: yalnizca eski varsayilan (<c>data/{ListFormCode}.json</c>) degerler tasinir.
|
||||||
|
/// </summary>
|
||||||
|
private async Task MigrateSeedFilePathsAsync(Guid? tenantId, string dataPath)
|
||||||
|
{
|
||||||
|
using (currentTenant.Change(tenantId))
|
||||||
|
{
|
||||||
|
using var uow = unitOfWorkManager.Begin(requiresNew: true, isTransactional: false);
|
||||||
|
|
||||||
|
var listForms = await listFormRepository.GetListAsync(a => a.SeedFilePath != null && a.SeedFilePath != "");
|
||||||
|
|
||||||
|
foreach (var listForm in listForms)
|
||||||
|
{
|
||||||
|
var filePath = SeedPathResolver.GetDataFilePath(listForm.ListFormCode);
|
||||||
|
if (!string.Equals(listForm.SeedFilePath?.Replace('\\', '/'), filePath, StringComparison.OrdinalIgnoreCase))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (!Directory.Exists(Path.Combine(dataPath, listForm.ListFormCode)))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
listForm.SeedFilePath = SeedPathResolver.GetDataFolderPath(listForm.ListFormCode);
|
||||||
|
await listFormRepository.UpdateAsync(listForm);
|
||||||
|
|
||||||
|
logger.LogInformation(
|
||||||
|
"[{ListFormCode}] Seed file path migrated to the row folder '{SeedFilePath}'.",
|
||||||
|
listForm.ListFormCode, listForm.SeedFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
await uow.CompleteAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Dosya, bir ekranin satir klasorunun (<c>data/{ListFormCode}/</c>) altinda mi.</summary>
|
||||||
|
private static bool IsRowFile(string dataPath, string filePath)
|
||||||
|
{
|
||||||
|
return !string.Equals(Path.GetDirectoryName(filePath), dataPath, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tek seferde uygulanan bir ekranin dosyalari: tek dosya ya da satir klasorundeki butun satir
|
||||||
|
/// dosyalari. <see cref="Path"/> kapsam klasorune goreli hedeftir (dosya ya da klasor) ve
|
||||||
|
/// sonuclarda, loglarda bu gorunur.
|
||||||
|
/// </summary>
|
||||||
|
private sealed record SeedDataTarget(string Path, string[] FilePaths);
|
||||||
|
|
||||||
private static int ReadOrder(string filePath)
|
private static int ReadOrder(string filePath)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|
@ -204,39 +268,49 @@ public class ListFormSeedDataApplier(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Dosyayi okur ve satirlari ilgili ListForm'un tablosuna uygular. Tek bir dosyanin hatasi
|
/// Hedefteki satirlari ilgili ListForm'un tablosuna uygular. Ekran tanimi, alan listesi ve veri
|
||||||
/// diger dosyalari etkilemez; hata sonuca tasinir ve loglanir.
|
/// kaynagi hedef basina bir kez cozulur; satir klasorundeki her dosya icin tekrarlanmaz — aksi
|
||||||
|
/// halde binlerce satirlik bir listede seed suresi katlanir. Tek bir hedefin hatasi digerlerini
|
||||||
|
/// etkilemez; hata sonuca tasinir ve loglanir.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task<ListFormSeedDataApplyResult> ApplyFileAsync(string filePath, bool overwriteExisting)
|
private async Task<ListFormSeedDataApplyResult> ApplyTargetAsync(SeedDataTarget target, bool overwriteExisting)
|
||||||
{
|
{
|
||||||
var fileName = Path.GetFileName(filePath);
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var seedFile = JsonSerializer.Deserialize<ListFormSeedDataFileDto>(
|
var seedFiles = new List<ListFormSeedDataFileDto>();
|
||||||
await File.ReadAllTextAsync(filePath),
|
|
||||||
ListFormSeedDataFileDto.JsonOptions);
|
|
||||||
|
|
||||||
var listFormCode = string.IsNullOrWhiteSpace(seedFile?.ListFormCode)
|
foreach (var filePath in target.FilePaths)
|
||||||
? Path.GetFileNameWithoutExtension(filePath)
|
|
||||||
: seedFile!.ListFormCode.Trim();
|
|
||||||
|
|
||||||
if (seedFile?.Rows == null || seedFile.Rows.Count == 0)
|
|
||||||
{
|
{
|
||||||
logger.LogWarning("[{File}] No row found, skipped.", fileName);
|
var seedFile = JsonSerializer.Deserialize<ListFormSeedDataFileDto>(
|
||||||
return new ListFormSeedDataApplyResult(fileName, 0, 0, 0);
|
await File.ReadAllTextAsync(filePath),
|
||||||
|
ListFormSeedDataFileDto.JsonOptions);
|
||||||
|
|
||||||
|
if (seedFile?.Rows?.Count > 0)
|
||||||
|
seedFiles.Add(seedFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
var listFormCode = ResolveListFormCode(target, seedFiles);
|
||||||
|
|
||||||
|
if (seedFiles.Count == 0)
|
||||||
|
{
|
||||||
|
logger.LogWarning("[{Target}] No row found, skipped.", target.Path);
|
||||||
|
return new ListFormSeedDataApplyResult(target.Path, 0, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
var listForm = await listFormRepository.FirstOrDefaultAsync(a => a.ListFormCode == listFormCode);
|
var listForm = await listFormRepository.FirstOrDefaultAsync(a => a.ListFormCode == listFormCode);
|
||||||
if (listForm == null)
|
if (listForm == null)
|
||||||
{
|
{
|
||||||
logger.LogWarning("[{File}] ListForm '{ListFormCode}' not found, skipped.", fileName, listFormCode);
|
logger.LogWarning("[{Target}] ListForm '{ListFormCode}' not found, skipped.", target.Path, listFormCode);
|
||||||
return new ListFormSeedDataApplyResult(fileName, 0, 0, 0, $"ListForm '{listFormCode}' not found.");
|
return new ListFormSeedDataApplyResult(target.Path, 0, 0, 0, $"ListForm '{listFormCode}' not found.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var fields = await listFormFieldRepository.GetListAsync(a => a.ListFormCode == listFormCode);
|
var fields = await listFormFieldRepository.GetListAsync(a => a.ListFormCode == listFormCode);
|
||||||
var keyFieldName = string.IsNullOrWhiteSpace(seedFile.KeyFieldName)
|
|
||||||
? listForm.KeyFieldName
|
// Anahtar alan ekranin butun satir dosyalarinda aynidir; dosyada yoksa ekrandan gelir.
|
||||||
: seedFile.KeyFieldName;
|
var keyFieldName = seedFiles
|
||||||
|
.Select(a => a.KeyFieldName)
|
||||||
|
.FirstOrDefault(a => !string.IsNullOrWhiteSpace(a))
|
||||||
|
?? listForm.KeyFieldName;
|
||||||
|
|
||||||
var (repository, connectionString, dataSourceType) = await dynamicDataManager.GetAsync(listForm.IsTenant, listForm.DataSourceCode);
|
var (repository, connectionString, dataSourceType) = await dynamicDataManager.GetAsync(listForm.IsTenant, listForm.DataSourceCode);
|
||||||
|
|
||||||
|
|
@ -244,7 +318,7 @@ public class ListFormSeedDataApplier(
|
||||||
var updated = 0;
|
var updated = 0;
|
||||||
var skipped = 0;
|
var skipped = 0;
|
||||||
|
|
||||||
foreach (var row in seedFile.Rows)
|
foreach (var row in seedFiles.SelectMany(a => a.Rows))
|
||||||
{
|
{
|
||||||
switch (await ApplyRowAsync(listForm, fields, keyFieldName, row, repository, connectionString, dataSourceType, overwriteExisting))
|
switch (await ApplyRowAsync(listForm, fields, keyFieldName, row, repository, connectionString, dataSourceType, overwriteExisting))
|
||||||
{
|
{
|
||||||
|
|
@ -261,18 +335,37 @@ public class ListFormSeedDataApplier(
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.LogInformation(
|
logger.LogInformation(
|
||||||
"[{File}] {Inserted} row(s) inserted, {Updated} row(s) updated, {Skipped} existing row(s) skipped.",
|
"[{Target}] {Inserted} row(s) inserted, {Updated} row(s) updated, {Skipped} existing row(s) skipped.",
|
||||||
fileName, inserted, updated, skipped);
|
target.Path, inserted, updated, skipped);
|
||||||
return new ListFormSeedDataApplyResult(fileName, inserted, updated, skipped);
|
return new ListFormSeedDataApplyResult(target.Path, inserted, updated, skipped);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// Tek bir dosyanin hatasi diger listelerin seed edilmesini engellemez.
|
// Tek bir hedefin hatasi diger listelerin seed edilmesini engellemez.
|
||||||
logger.LogError(ex, "Failed to process list form data seed file: {FileName}", fileName);
|
logger.LogError(ex, "Failed to process list form data seed target: {Target}", target.Path);
|
||||||
return new ListFormSeedDataApplyResult(fileName, 0, 0, 0, ex.Message);
|
return new ListFormSeedDataApplyResult(target.Path, 0, 0, 0, ex.Message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Hedefin ekran kodu. Dosyada yaziliysa oradan gelir; degilse yoldan cikarilir: satir klasorunde
|
||||||
|
/// klasorun adi, tek dosyada uzantisiz dosya adi ekran kodudur.
|
||||||
|
/// </summary>
|
||||||
|
private static string ResolveListFormCode(SeedDataTarget target, List<ListFormSeedDataFileDto> seedFiles)
|
||||||
|
{
|
||||||
|
var listFormCode = seedFiles
|
||||||
|
.Select(a => a.ListFormCode)
|
||||||
|
.FirstOrDefault(a => !string.IsNullOrWhiteSpace(a));
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(listFormCode))
|
||||||
|
return listFormCode.Trim();
|
||||||
|
|
||||||
|
// Ekran kodu nokta icerdigi icin uzanti yalnizca ".json" ile kesilir.
|
||||||
|
var name = Path.GetFileName(target.Path);
|
||||||
|
|
||||||
|
return name.EndsWith(".json", StringComparison.OrdinalIgnoreCase) ? name[..^".json".Length] : name;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Satiri veritabanina yazar. Kayit yoksa eklenir; varsa <paramref name="overwriteExisting"/>
|
/// Satiri veritabanina yazar. Kayit yoksa eklenir; varsa <paramref name="overwriteExisting"/>
|
||||||
/// true ise dosyadaki degerlerle guncellenir ve soft delete izleri temizlenir, degilse dokunulmaz.
|
/// true ise dosyadaki degerlerle guncellenir ve soft delete izleri temizlenir, degilse dokunulmaz.
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,47 @@ public static class SeedPathResolver
|
||||||
return $"{DataFolder}/{listFormCode}.json";
|
return $"{DataFolder}/{listFormCode}.json";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bir ListForm icin satir basina dosya tutan veri seed klasorunun kapsam klasorune gore goreli
|
||||||
|
/// yolu. Cok satirli ya da satiri buyuk (bilesen kodu gibi) ekranlarda tek dosya her kayitta
|
||||||
|
/// bastan yazilir; klasor modunda yalnizca islem goren satirin dosyasina dokunulur.
|
||||||
|
/// </summary>
|
||||||
|
public static string GetDataFolderPath(string listFormCode)
|
||||||
|
{
|
||||||
|
return $"{DataFolder}/{listFormCode}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <c>ListForm.SeedFilePath</c> tek bir dosyayi mi yoksa satir basina dosya tutan bir klasoru mu
|
||||||
|
/// gosteriyor. Ayrim uzantidan yapilir: <c>.json</c> ile biten deger dosyadir, digerleri klasor.
|
||||||
|
/// Klasor diskte henuz yoksa da karar verilebilsin diye dosya sistemine bakilmaz.
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsSeedFolderPath(string? seedFilePath)
|
||||||
|
{
|
||||||
|
var normalized = seedFilePath?.Replace('\\', '/').Trim('/');
|
||||||
|
|
||||||
|
return !string.IsNullOrWhiteSpace(normalized)
|
||||||
|
&& !normalized.EndsWith(".json", StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Klasor modunda bir satirin dosya adi: anahtar degeri. Dosya adinda kullanilamayan karakterler
|
||||||
|
/// (kod tipi anahtarlarda gorulebilir) alt cizgiye cevrilir; anahtar bos ise null doner — o satir
|
||||||
|
/// icin dosya yolu uretilemez.
|
||||||
|
/// </summary>
|
||||||
|
public static string? GetRowFileName(string? key)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(key))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var invalidChars = Path.GetInvalidFileNameChars();
|
||||||
|
var name = new string([.. key.Trim().Select(c => invalidChars.Contains(c) ? '_' : c)]);
|
||||||
|
|
||||||
|
return name.Length == 0 ? null : $"{name}.json";
|
||||||
|
}
|
||||||
|
|
||||||
private const string RepositorySeedsFolder = "seeds";
|
private const string RepositorySeedsFolder = "seeds";
|
||||||
private const string RepositoryConfigsFolder = "configs";
|
private const string RepositoryConfigsFolder = "configs";
|
||||||
private const string FallbackFolderName = "Seeds";
|
private const string FallbackFolderName = "Seeds";
|
||||||
|
|
|
||||||
23
claude.md
23
claude.md
|
|
@ -231,10 +231,10 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` →
|
||||||
seed dosyasını görür. Seed dosyası `ui/` dışında olduğu için aynı sınıflar `ui/safelist.txt`
|
seed dosyasını görür. Seed dosyası `ui/` dışında olduğu için aynı sınıflar `ui/safelist.txt`
|
||||||
sonuna da yazılır (`twSafelistGenerator`, derlemede otomatik); yoksa uygulama sunucuda stilsiz
|
sonuna da yazılır (`twSafelistGenerator`, derlemede otomatik); yoksa uygulama sunucuda stilsiz
|
||||||
kalır. İkinci bir safelist dosyası açma.
|
kalır. İkinci bir safelist dosyası açma.
|
||||||
- **Custom component kaydı/silinmesi** → `data/App.DeveloperKit.CustomComponents.json`
|
- **Custom component kaydı/silinmesi** → `data/App.DeveloperKit.CustomComponents/{Id}.json`
|
||||||
(Custom Components ekranının seed verisi); bileşenin kendi dosyası yoktur, her bileşen bu
|
(Custom Components ekranının seed verisi); ekran klasör modundadır, her bileşen kendi satır
|
||||||
dosyada bir satırdır. Hem listeden hem Visual Designer'dan (`CustomComponentAppService`) yapılan
|
dosyasındadır. Hem listeden hem Visual Designer'dan (`CustomComponentAppService`) yapılan kayıt
|
||||||
kayıt aynı dosyaya yansır; kullandığı endpoint'ler `crud/{Entity}.json`.
|
aynı klasöre yansır; kullandığı endpoint'ler `crud/{Entity}.json`.
|
||||||
- **Wizard dosyası** → tek başına taşınmaz; export zip'i **özyinelemeli bağımlılık kapanışını**
|
- **Wizard dosyası** → tek başına taşınmaz; export zip'i **özyinelemeli bağımlılık kapanışını**
|
||||||
taşır (`WizardExportCollector`): alt formların wizard dosyaları, ekranın `SeedFilePath` veri
|
taşır (`WizardExportCollector`): alt formların wizard dosyaları, ekranın `SeedFilePath` veri
|
||||||
aynası, lookup sorgularının işaret ettiği nesnelerin scriptleri, `crud/` ve
|
aynası, lookup sorgularının işaret ettiği nesnelerin scriptleri, `crud/` ve
|
||||||
|
|
@ -269,8 +269,17 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` →
|
||||||
olmasıdır: tabloya dair bir bulgu üretirken `TableName` (+ tablo hedefte yoksa `TableMissing`)
|
olmasıdır: tabloya dair bir bulgu üretirken `TableName` (+ tablo hedefte yoksa `TableMissing`)
|
||||||
doldurulur, istemci tablo tasarımcısını buna göre açar.
|
doldurulur, istemci tablo tasarımcısını buna göre açar.
|
||||||
- **`ListForm.SeedFilePath` dolu bir ekran** → `SeedSyncInsert/SeedSyncUpdate/SeedSyncDelete`
|
- **`ListForm.SeedFilePath` dolu bir ekran** → `SeedSyncInsert/SeedSyncUpdate/SeedSyncDelete`
|
||||||
bayraklarından işaretli olan işlemler kapsam klasöründeki `data/{ListFormCode}.json` dosyasına
|
bayraklarından işaretli olan işlemler kapsam klasöründeki hedefe yansır
|
||||||
yansır (`ListFormSeedDataSynchronizer`); migrate/seed sırasında ve wizard import'unun kapanışında
|
(`ListFormSeedDataSynchronizer`). Hedef iki biçimdedir ve ayrım uzantıdadır: `.json` ile biten
|
||||||
|
değer **tek dosyadır** (`data/{ListFormCode}.json`, bütün satırlar orada), uzantısız değer
|
||||||
|
**klasördür** (`data/{ListFormCode}/{anahtar}.json`, her satır kendi dosyasında) ve klasör yoksa
|
||||||
|
ilk kayıtta oluşturulur. Klasör modu satırı büyük ya da sık yazılan ekranlar içindir
|
||||||
|
(varsayılan tek dosya; şu an klasör kullanan tek ekran Custom Components). Applier `data/`
|
||||||
|
altını özyinelemeli tarar ve **bir klasörün tamamını tek unit of work'te** uygular — ekran
|
||||||
|
tanımı, alan listesi ve veri kaynağı satır dosyası başına değil ekran başına bir kez çözülür;
|
||||||
|
dosyadaki `ListFormCode` boşsa ekran kodu klasör adından gelir. Tek dosyaya bağlı eski bir kayıt,
|
||||||
|
klasörü diskte varsa migrate sırasında klasöre taşınır.
|
||||||
|
Migrate/seed sırasında ve wizard import'unun kapanışında
|
||||||
(`CompleteImport`) `ListFormSeedDataApplier` dosyayı geri uygular. Migrate/seed'de kayıt yoksa
|
(`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
|
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
|
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
|
||||||
|
|
@ -297,7 +306,7 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` →
|
||||||
(menü ucu `[Authorize]`). Sayfa ise Custom Component'tir: `/admin/` ile
|
(menü ucu `[Authorize]`). Sayfa ise Custom Component'tir: `/admin/` ile
|
||||||
başlamayan `RoutePath` otomatik public route olur ve aynı yolu paylaşan fiziksel route'u gölgeler;
|
başlamayan `RoutePath` otomatik public route olur ve aynı yolu paylaşan fiziksel route'u gölgeler;
|
||||||
`RoutePath` boş bırakılabilir (rotasız bileşen). Yeni public sayfa =
|
`RoutePath` boş bırakılabilir (rotasız bileşen). Yeni public sayfa =
|
||||||
`data/App.DeveloperKit.CustomComponents.json` satırı + `Pub` menü kaydı; ikisi birlikte yazılır.
|
`data/App.DeveloperKit.CustomComponents/{Id}.json` satırı + `Pub` menü kaydı; ikisi birlikte yazılır.
|
||||||
Üst ve alt bilgi rotasız bileşenlerdir (`PublicHeader`, `PublicFooter`), layout ikisini de
|
Üst ve alt bilgi rotasız bileşenlerdir (`PublicHeader`, `PublicFooter`), layout ikisini de
|
||||||
`renderComponent` ile çizer; rotasız bileşende `RoutePath` **NULL** yazılır (tekil indeks yalnızca
|
`renderComponent` ile çizer; rotasız bileşende `RoutePath` **NULL** yazılır (tekil indeks yalnızca
|
||||||
dolu değerleri kapsar). Üst bilginin davranışı `components/publicSite` katalog düğümlerinde,
|
dolu değerleri kapsar). Üst bilginin davranışı `components/publicSite` katalog düğümlerinde,
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"ListFormCode": "App.DeveloperKit.CustomComponents",
|
||||||
|
"KeyFieldName": "Id",
|
||||||
|
"GeneratedAt": "2026-09-21T14:46:03.8784173Z",
|
||||||
|
"Order": 0,
|
||||||
|
"Rows": [
|
||||||
|
{
|
||||||
|
"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": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"ListFormCode": "App.DeveloperKit.CustomComponents",
|
||||||
|
"KeyFieldName": "Id",
|
||||||
|
"GeneratedAt": "2026-09-21T14:46:03.8784173Z",
|
||||||
|
"Order": 0,
|
||||||
|
"Rows": [
|
||||||
|
{
|
||||||
|
"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 <DynamicEntityComponent id=\"c_mdljvvmq_fno52v\" title={title} />\n );\n};\n\nexport default RoleListComponent;",
|
||||||
|
"Props": null,
|
||||||
|
"DataSources": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"ListFormCode": "App.DeveloperKit.CustomComponents",
|
||||||
|
"KeyFieldName": "Id",
|
||||||
|
"GeneratedAt": "2026-09-21T14:46:03.8784173Z",
|
||||||
|
"Order": 0,
|
||||||
|
"Rows": [
|
||||||
|
{
|
||||||
|
"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<DynamicEntityComponentProps> = ({ title }) => {\n const [data, setData] = useState<Array<{ id: string; name: string }>>([]);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<string | null>(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 <div>Loading...</div>;\n if (error) return <div className=\"text-red-600 dark:text-red-400\">Error: {error}</div>;\n if (!data.length) return <div>No records found</div>;\n\n const headers = [\"id\", \"name\", \"actions\"];\n\n return (\n <div className=\"overflow-auto\">\n <table className=\"min-w-full bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700 shadow-sm rounded-lg\">\n <thead className=\"bg-slate-100 dark:bg-slate-800\">\n <tr>\n {headers.map((key) => (\n <th\n key={key}\n className=\"text-left px-4 py-2 border-b border-slate-200 dark:border-slate-700 text-sm font-medium text-slate-700 dark:text-slate-200\"\n >\n {key === \"actions\" ? \"Actions\" : key}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {data.map((item, rowIndex) => (\n <tr key={item.id ?? rowIndex} className=\"hover:bg-slate-50 dark:hover:bg-slate-800\">\n <td className=\"px-4 py-2 border-b border-slate-100 dark:border-slate-800 text-sm text-slate-800 dark:text-slate-100\">\n {item.id}\n </td>\n <td className=\"px-4 py-2 border-b border-slate-100 dark:border-slate-800 text-sm text-slate-800 dark:text-slate-100\">\n {item.name}\n </td>\n <td className=\"px-4 py-2 border-b border-slate-100 dark:border-slate-800\">\n <Button\n type=\"button\"\n color=\"blue-500\"\n onClick={() => alert(item.name)}\n shape=\"round\"\n size=\"xs\"\n variant=\"solid\"\n id=\"c_mrix4c1a_qrybhk\"\n >\n Show Name\n </Button>\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n );\n};\n\nexport default DynamicEntityComponent;",
|
||||||
|
"Props": null,
|
||||||
|
"DataSources": "",
|
||||||
|
"LastModificationTime": null,
|
||||||
|
"CreationTime": "2026-09-05T17:22:11"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -405,7 +405,7 @@ function FormTabDatabaseDataSource(props: FormEditProps) {
|
||||||
type="text"
|
type="text"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
name="seedFilePath"
|
name="seedFilePath"
|
||||||
placeholder="data/{ListFormCode}.json"
|
placeholder="data/{ListFormCode}.json | data/{ListFormCode}"
|
||||||
component={Input}
|
component={Input}
|
||||||
/>
|
/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@ export default {
|
||||||
// Ayni siniflar `safelist.txt` dosyasina da yazilir (twSafelistGenerator): derleme baglami depo
|
// Ayni siniflar `safelist.txt` dosyasina da yazilir (twSafelistGenerator): derleme baglami depo
|
||||||
// kokunu gormedigin de — docker, ayri checkout — bu glob bos doner, safelist ise `ui/` icinde
|
// kokunu gormedigin de — docker, ayri checkout — bu glob bos doner, safelist ise `ui/` icinde
|
||||||
// oldugu icin her ortamda taranir.
|
// oldugu icin her ortamda taranir.
|
||||||
|
// Bilesenler satir basina dosya tutan klasorde durur; tek dosya bicimi eski kurulumlarda kalir.
|
||||||
|
'../configs/seeds/*/data/App.DeveloperKit.CustomComponents/*.json',
|
||||||
'../configs/seeds/*/data/App.DeveloperKit.CustomComponents.json',
|
'../configs/seeds/*/data/App.DeveloperKit.CustomComponents.json',
|
||||||
],
|
],
|
||||||
safelist: [
|
safelist: [
|
||||||
|
|
|
||||||
|
|
@ -59,9 +59,17 @@ const collectComponentClasses = () => {
|
||||||
const files = fs
|
const files = fs
|
||||||
.readdirSync(SEEDS_ROOT, { withFileTypes: true })
|
.readdirSync(SEEDS_ROOT, { withFileTypes: true })
|
||||||
.filter((entry) => entry.isDirectory())
|
.filter((entry) => entry.isDirectory())
|
||||||
.map((entry) =>
|
.flatMap((entry) => {
|
||||||
nodePath.join(SEEDS_ROOT, entry.name, 'data/App.DeveloperKit.CustomComponents.json'),
|
const base = nodePath.join(SEEDS_ROOT, entry.name, 'data/App.DeveloperKit.CustomComponents')
|
||||||
)
|
|
||||||
|
// Her bilesen klasorun altinda kendi dosyasindadir; tek dosya bicimi eski kurulumlarda kalir.
|
||||||
|
return fs.existsSync(base)
|
||||||
|
? fs
|
||||||
|
.readdirSync(base)
|
||||||
|
.filter((name) => name.endsWith('.json'))
|
||||||
|
.map((name) => nodePath.join(base, name))
|
||||||
|
: [`${base}.json`]
|
||||||
|
})
|
||||||
.filter((file) => fs.existsSync(file))
|
.filter((file) => fs.existsSync(file))
|
||||||
|
|
||||||
const classes = new Set()
|
const classes = new Set()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue