Custom Component ve Wizard Import/Export özelliği
This commit is contained in:
parent
54e3390cbf
commit
59ec8950a3
46 changed files with 1627 additions and 1770 deletions
40
.github/instructions/ai.instructions.md
vendored
40
.github/instructions/ai.instructions.md
vendored
|
|
@ -294,8 +294,8 @@ the Wizard's `Custom` path.
|
||||||
per-command Form permissions (`selectPermission`, `insertPermission`, `updatePermission`,
|
per-command Form permissions (`selectPermission`, `insertPermission`, `updatePermission`,
|
||||||
`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.
|
||||||
- Manager screen permissions: `App.DeveloperKit.Components{,.Create,.Update,.Delete}`.
|
- List screen permissions: `App.DeveloperKit.CustomComponents{,.Create,.Update,.Delete}`.
|
||||||
- Every save/delete writes `configs/seeds/{scope}/custom/{Name}.json`; the endpoints it uses are
|
- Every save/delete is mirrored into `configs/seeds/{scope}/data/App.DeveloperKit.CustomComponents.json`; the endpoints it uses are
|
||||||
referenced through `crud/{Entity}.json`. A component that is not seed-backed is not done.
|
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
|
||||||
|
|
@ -655,15 +655,16 @@ a deploy. AI must learn and teach implementation flow primarily from these asset
|
||||||
- PermissionsData.json
|
- PermissionsData.json
|
||||||
- HostData.json
|
- HostData.json
|
||||||
- LanguagesData.json
|
- LanguagesData.json
|
||||||
- WizardDataSeeder.cs / CustomComponentDataSeeder.cs / CrudDataSeeder.cs — the seeders that read
|
- WizardDataSeeder.cs / CrudDataSeeder.cs — the seeders that read
|
||||||
surface B below
|
surface B below
|
||||||
|
|
||||||
**B. Runtime seeds (written by the running application)** — `configs/seeds/`, resolved through
|
**B. Runtime seeds (written by the running application)** — `configs/seeds/`, resolved through
|
||||||
`SeedPathResolver`. The Wizard, Component Manager and CRUD Endpoint Manager write here; the
|
`SeedPathResolver`. The Wizard, the Custom Components screen / Visual Designer and the CRUD
|
||||||
|
Endpoint Manager write here; the
|
||||||
seeders above restore them when the database is recreated. Scope folders mirror the CDN layout:
|
seeders above restore them when the database is recreated. Scope folders mirror the CDN layout:
|
||||||
|
|
||||||
```
|
```
|
||||||
configs/seeds/host/{wizard,custom,crud,sql/{object,execute},postgres/{object,execute}}
|
configs/seeds/host/{wizard,data,crud,sql/{object,execute},postgres/{object,execute}}
|
||||||
configs/seeds/tenants/{tenantId}/… same layout, tenant scope
|
configs/seeds/tenants/{tenantId}/… same layout, tenant scope
|
||||||
configs/seeds/.imports/{importId}/… wizard import staging + backup (not scanned)
|
configs/seeds/.imports/{importId}/… wizard import staging + backup (not scanned)
|
||||||
```
|
```
|
||||||
|
|
@ -891,7 +892,8 @@ Exporting a wizard produces a zip whose paths are relative to the scope folder
|
||||||
(`host` / `tenants/{tenantId}`) and which carries every dependency:
|
(`host` / `tenants/{tenantId}`) and which carries every dependency:
|
||||||
|
|
||||||
- `wizard/{file}.json`
|
- `wizard/{file}.json`
|
||||||
- `custom/{component}.json` (with its dependencies) — `Custom` path
|
- `data/App.DeveloperKit.CustomComponents.json` — `Custom` path; the file is filtered down to the
|
||||||
|
exported component and its dependency chain, never exported whole
|
||||||
- `crud/{entity}.json` — endpoints those components use
|
- `crud/{entity}.json` — endpoints those components use
|
||||||
- `{sql|postgres}/{object|execute}/{object}.sql` — the object behind `SelectCommand` on the
|
- `{sql|postgres}/{object|execute}/{object}.sql` — the object behind `SelectCommand` on the
|
||||||
`List` path
|
`List` path
|
||||||
|
|
@ -899,9 +901,29 @@ Exporting a wizard produces a zip whose paths are relative to the scope folder
|
||||||
Import is two-phase: `AnalyzeImport` (unzip to `.imports/{id}/staged`, classify each entry as
|
Import is two-phase: `AnalyzeImport` (unzip to `.imports/{id}/staged`, classify each entry as
|
||||||
`New` / `Identical` / `Conflict`, conflicts resolved in a diff editor) then `ApplyImport`
|
`New` / `Identical` / `Conflict`, conflicts resolved in a diff editor) then `ApplyImport`
|
||||||
(file-by-file write, previous content backed up so `RollbackImport` can undo the whole batch),
|
(file-by-file write, previous content backed up so `RollbackImport` can undo the whole batch),
|
||||||
closed by `CompleteImport`. Limits: allowed root folders `wizard`, `crud`, `custom`, `sql`,
|
closed by `CompleteImport`.
|
||||||
`postgres` (and only `object` / `execute` under the SQL provider folders); 5 MB per entry, 50 MB
|
|
||||||
per archive, 500 entries. Permissions: `App.Listforms.Wizard.Export` / `.Import`.
|
A `data/*.json` entry is compared and merged **by row, not by file**: the diff shows only the
|
||||||
|
rows whose keys the archive carries (file-level `GeneratedAt`/`Order` are excluded), so unrelated
|
||||||
|
records never surface as a difference — no matching row means `New` (insert), a matching identical
|
||||||
|
row means `Identical` (nothing is transferred), a matching different row means `Conflict` (update).
|
||||||
|
The file itself is never overwritten: rows are merged into the target by key (incoming row wins,
|
||||||
|
target-only rows are kept). `CompleteImport` then applies exactly the
|
||||||
|
`data/` files it wrote to the database through `IListFormSeedDataApplier` with
|
||||||
|
`overwriteExisting: true` — a missing row is inserted, an existing one is updated from the file
|
||||||
|
(unlike migrate/seed, where a possibly stale file must not overwrite live data), and a row that was
|
||||||
|
soft deleted is revived by clearing the columns the screen's `DeleteCommand` sets
|
||||||
|
(`IsDeleted`/`DeletionTime`/`DeleterId`), otherwise the default filter would keep hiding it — and
|
||||||
|
reports the inserted/updated/skipped counts. Data files that were already identical to the target
|
||||||
|
are applied too: there was nothing to write, but the rows may be missing or soft deleted in the
|
||||||
|
database. A file the user chose to keep (different, not written) is left alone. This step is not
|
||||||
|
covered by `RollbackImport`, which only restores files and must be run before it. The other
|
||||||
|
folders (`wizard`, `crud`, `sql`) stay as files and reach the database through the seeders or a
|
||||||
|
wizard deploy.
|
||||||
|
|
||||||
|
Limits: allowed root folders `wizard`, `crud`, `data`, `sql`, `postgres` (and only `object` /
|
||||||
|
`execute` under the SQL provider folders); 5 MB per entry, 50 MB per archive, 500 entries.
|
||||||
|
Permissions: `App.Listforms.Wizard.Export` / `.Import`.
|
||||||
|
|
||||||
AI must not propose moving a screen between environments by copying a single json file.
|
AI must not propose moving a screen between environments by copying a single json file.
|
||||||
|
|
||||||
|
|
|
||||||
2
.github/instructions/list.instructions.md
vendored
2
.github/instructions/list.instructions.md
vendored
|
|
@ -8,7 +8,7 @@ This document summarizes the rules, standards, and step-by-step instructions for
|
||||||
|
|
||||||
|
|
||||||
> **Bu doküman depo içi seeder'larla (DbMigrator/Migrations) modül eklemeyi anlatır.**
|
> **Bu doküman depo içi seeder'larla (DbMigrator/Migrations) modül eklemeyi anlatır.**
|
||||||
> Çalışma zamanında Wizard / Component Manager / CRUD Endpoint Manager ile üretim yapacaksan
|
> Çalışma zamanında Wizard / Custom Components / CRUD Endpoint Manager ile üretim yapacaksan
|
||||||
> `lowcode.instructions.md` esastır; oradaki seed dosyaları `configs/seeds` altına düşer.
|
> `lowcode.instructions.md` esastır; oradaki seed dosyaları `configs/seeds` altına düşer.
|
||||||
|
|
||||||
## General Principles
|
## General Principles
|
||||||
|
|
|
||||||
|
|
@ -131,7 +131,7 @@ ve çoğu isteğin cevabı aslında oradadır:
|
||||||
| `CustomJsSourcesJson` · `CustomStyleSourcesJson` | Sayfaya enjekte edilen JS/CSS | §5.5 |
|
| `CustomJsSourcesJson` · `CustomStyleSourcesJson` | Sayfaya enjekte edilen JS/CSS | §5.5 |
|
||||||
| `AdaptiveLayoutJson` | Dar ekranda öğe gizleme (grafik) | §8.4 |
|
| `AdaptiveLayoutJson` | Dar ekranda öğe gizleme (grafik) | §8.4 |
|
||||||
| `CommonJson` · `DataSourceJson` | Grafik geneli ve grafik sorgusu | §8.2 · §8.3 |
|
| `CommonJson` · `DataSourceJson` | Grafik geneli ve grafik sorgusu | §8.2 · §8.3 |
|
||||||
| `Width` · `Height` · `FullHeight` | Ekran boyutu | — |
|
| `Width` · `Height` · `FullHeight` | Ekran boyutu. `Height > 0` sabit piksel yüksekliği verir. Grid/Tree/Gantt/Scheduler'da `FullHeight` bayrağı görünümü `calc(100vh - …)` ile sayfaya yayar; **TodoBoard** bayrağa bakmaz, `Height` verilmediyse panoyu her zaman sayfanın kalan yüksekliğine yayar (kolonlar kendi içinde kayar), alt form olarak açıldığında ise içeriği kadar yer kaplar. | — |
|
||||||
| `ListFormType` · `IsSubForm` · `SubFormsListFormType` · `ShowNote` | Ekranın türü ve alt form davranışı | §5.6 |
|
| `ListFormType` · `IsSubForm` · `SubFormsListFormType` · `ShowNote` | Ekranın türü ve alt form davranışı | §5.6 |
|
||||||
| `CultureName` · `UserId` · `RoleId` | Aynı ekranın dil/kullanıcı/rol varyantı | §5.7 |
|
| `CultureName` · `UserId` · `RoleId` | Aynı ekranın dil/kullanıcı/rol varyantı | §5.7 |
|
||||||
|
|
||||||
|
|
@ -531,6 +531,9 @@ ListForm editöründeki tipli formdan düzenlenemez; kalıcı kullanılacaksa C#
|
||||||
`PriorityExpr`, `CompletedExpr`, `OrderExpr`, `TimesheetExpr`, `SubTaskExpr`,
|
`PriorityExpr`, `CompletedExpr`, `OrderExpr`, `TimesheetExpr`, `SubTaskExpr`,
|
||||||
`StatusOrder` (kolon sırası, virgüllü), `AllowDragging` (varsayılan `true`).
|
`StatusOrder` (kolon sırası, virgüllü), `AllowDragging` (varsayılan `true`).
|
||||||
|
|
||||||
|
Panonun yüksekliği `TodoOptionJson` ile değil ekranın `Height` kolonuyla belirlenir; boş
|
||||||
|
bırakıldığında pano sayfanın kalan yüksekliğini kaplar ve her kolon kendi içinde kayar.
|
||||||
|
|
||||||
> `*` işaretli alan boşsa seeder o görünümün JSON'unu **yazmaz**; bayrağı açmak yetmez.
|
> `*` işaretli alan boşsa seeder o görünümün JSON'unu **yazmaz**; bayrağı açmak yetmez.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -1183,7 +1186,7 @@ soneklerini alır.
|
||||||
| **Developer Kit › SQL Query Manager** | `App.SqlQueryManager` · `/admin/sqlQueryManager` | — | — | View/procedure/function yazma ve çalıştırma; karar sırasının 2. adımı buradan başlar. |
|
| **Developer Kit › 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.Components` · `/admin/developerkit/components` | `Sas_H_CustomComponent` | host | Custom component listesi ve **Visual Designer** girişi (§10). |
|
| **Developer Kit › Components** | `App.DeveloperKit.CustomComponents` · `/admin/list/App.DeveloperKit.CustomComponents` | `Sas_H_CustomComponent` | host | Custom component listesi (ListForm) ve satırdaki **Design** butonuyla **Visual Designer** girişi (§10). Kayıtlar `data/App.DeveloperKit.CustomComponents.json` dosyasına senkronlanır. |
|
||||||
| **Developer Kit › ListForm** | `App.Listforms.Listform` | `Sas_H_ListForm` · `Sas_H_ListFormField` · `Sas_H_ListFormWorkflow` · `Sas_H_ListFormCustomization` · `Sas_H_ListFormImport` | host | Ekran tanımlarının kendisi. Bu dosyada anlatılan bütün JSON kolonlarının canlı hâli; düzenleyicisi `/admin/listform/edit/{kod}`. |
|
| **Developer Kit › 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`. |
|
||||||
| **Forum Management** | `App.ForumManagement` (grup menüsü) | `Sas_T_ForumCategory` · `Sas_T_ForumTopic` · `Sas_T_ForumPost` | tenant | Altında Categories / Topics / Posts ListForm ekranları; yayın tarafı Administration'daki `/admin/forum`. |
|
| **Forum Management** | `App.ForumManagement` (grup menüsü) | `Sas_T_ForumCategory` · `Sas_T_ForumTopic` · `Sas_T_ForumPost` | tenant | Altında Categories / Topics / Posts ListForm ekranları; yayın tarafı Administration'daki `/admin/forum`. |
|
||||||
|
|
@ -1272,7 +1275,7 @@ görünürlük `checkPermission` ile bileşen içinde kurulur (`lowcode` §7.5).
|
||||||
| `/admin/listform/wizardManager` | `listForm/wizard/WizardFileManager` | `App.Listforms.Wizard` |
|
| `/admin/listform/wizardManager` | `listForm/wizard/WizardFileManager` | `App.Listforms.Wizard` |
|
||||||
| `/admin/listform/edit/:listFormCode` | `listForm/edit/FormEdit` | `[]` — yetki ListForm tarafında |
|
| `/admin/listform/edit/:listFormCode` | `listForm/edit/FormEdit` | `[]` — yetki ListForm tarafında |
|
||||||
| `/admin/sqlQueryManager` | `developerKit/SqlQueryManager` | `App.SqlQueryManager` |
|
| `/admin/sqlQueryManager` | `developerKit/SqlQueryManager` | `App.SqlQueryManager` |
|
||||||
| `/admin/developerkit/components{,/edit/:id}` | `ComponentManagerPage` · `ComponentCodeLayout` | `App.DeveloperKit.Components` |
|
| `/admin/developerkit/components/design/:id` | `ComponentCodeLayout` (liste tarafı ListForm ekranıdır) | `App.DeveloperKit.CustomComponents` |
|
||||||
| `/admin/reports/:report/{view,design}/:id?/:listFormCode?` | `DevexpressReportViewer` · `DevexpressReportDesigner` | — |
|
| `/admin/reports/:report/{view,design}/:id?/:listFormCode?` | `DevexpressReportViewer` · `DevexpressReportDesigner` | — |
|
||||||
| `/admin/public/{home,about,services,contact}/designer` | ilgili public sayfa (tasarım modu) | `App.Home` · `App.About` · `App.Services` · `App.Contact` |
|
| `/admin/public/{home,about,services,contact}/designer` | ilgili public sayfa (tasarım modu) | `App.Home` · `App.About` · `App.Services` · `App.Contact` |
|
||||||
| `/admin/settings` | `settings/Settings` | `App.Setting` |
|
| `/admin/settings` | `settings/Settings` | `App.Setting` |
|
||||||
|
|
|
||||||
58
.github/instructions/lowcode.instructions.md
vendored
58
.github/instructions/lowcode.instructions.md
vendored
|
|
@ -32,7 +32,7 @@ Bir istek geldiğinde çekirdek koda dokunmak değil, `configs/seeds/` altına u
|
||||||
## 0. Üretim protokolü (her prompt için zorunlu)
|
## 0. Üretim protokolü (her prompt için zorunlu)
|
||||||
|
|
||||||
Kullanıcı "şöyle bir ekran/form/süreç istiyorum" dediğinde **uygulamayı kullanıyormuş gibi**
|
Kullanıcı "şöyle bir ekran/form/süreç istiyorum" dediğinde **uygulamayı kullanıyormuş gibi**
|
||||||
davran: Wizard'ın, Component Manager'ın ve SQL Query Manager'ın diske yazacağı dosyaların
|
davran: Wizard'ın, Custom Components ekranının ve SQL Query Manager'ın diske yazacağı dosyaların
|
||||||
aynılarını `configs/seeds/host/` altına sen yaz. Ekranlara tıklamanın yerine geçen şey budur.
|
aynılarını `configs/seeds/host/` altına sen yaz. Ekranlara tıklamanın yerine geçen şey budur.
|
||||||
|
|
||||||
Talep iki biçimde gelir ve ikisi de aynı hatta girer:
|
Talep iki biçimde gelir ve ikisi de aynı hatta girer:
|
||||||
|
|
@ -52,9 +52,10 @@ configs/seeds/host/
|
||||||
├── sql/execute/{Ad}.sql ← bir kez çalışacak script (veri dolumu, migrasyon)
|
├── sql/execute/{Ad}.sql ← bir kez çalışacak script (veri dolumu, migrasyon)
|
||||||
├── postgres/object|execute/… ← aynısının PostgreSQL diyalekti (hedef PG ise)
|
├── postgres/object|execute/… ← aynısının PostgreSQL diyalekti (hedef PG ise)
|
||||||
├── crud/{EntityName}.json ← tablonun REST uçları
|
├── crud/{EntityName}.json ← tablonun REST uçları
|
||||||
├── custom/{ComponentName}.json ← Custom Component (Visual Designer dokümanı ya da kod)
|
|
||||||
├── wizard/{WizardName}.json ← ekran + menü + yetki + dil anahtarları
|
├── 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:
|
||||||
|
data/App.DeveloperKit.CustomComponents.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.
|
||||||
|
|
@ -65,7 +66,7 @@ Tenant kapsamı isteniyorsa `host/` yerine `tenants/{tenantId}/`; iç düzen ayn
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| 1 | Tablo / view | `sql/object/…` | Ekranın bağlanacağı nesne önce var olmalı |
|
| 1 | Tablo / view | `sql/object/…` | Ekranın bağlanacağı nesne önce var olmalı |
|
||||||
| 2 | CRUD uçları (gerekiyorsa) | `crud/{Entity}.json` | Custom Component'in data source'ları buna bakar |
|
| 2 | CRUD uçları (gerekiyorsa) | `crud/{Entity}.json` | Custom Component'in data source'ları buna bakar |
|
||||||
| 3 | Custom Component (gerekiyorsa) | `custom/{Name}.json` | Wizard `Custom` yolunda bileşen adını arar |
|
| 3 | Custom Component (gerekiyorsa) | `data/App.DeveloperKit.CustomComponents.json` | Wizard `Custom` yolunda bileşen adını arar |
|
||||||
| 4 | Wizard | `wizard/{Ad}.json` | Menü, yetki, dil anahtarı ve ListForm'u üretir |
|
| 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 |
|
||||||
|
|
@ -105,7 +106,7 @@ Uydurma. Şu üçünü mutlaka kontrol et:
|
||||||
| JSON alan adları | **PascalCase** (`Wizard`, `ListFormCode`, `Groups`) — seeder `PropertyNameCaseInsensitive` okur ama dosyalar PascalCase üretilir |
|
| JSON alan adları | **PascalCase** (`Wizard`, `ListFormCode`, `Groups`) — seeder `PropertyNameCaseInsensitive` okur ama dosyalar PascalCase üretilir |
|
||||||
| Custom component dosyası | Kök alanlar PascalCase; `Props` **içindeki** designer dokümanı camelCase (`visualDesigner`, `nodes`, `sourceMode`) |
|
| Custom component dosyası | Kök alanlar PascalCase; `Props` **içindeki** designer dokümanı camelCase (`visualDesigner`, `nodes`, `sourceMode`) |
|
||||||
| Enum'lar | Sayı olarak yazılır (§3) |
|
| Enum'lar | Sayı olarak yazılır (§3) |
|
||||||
| `GeneratedAt` | ISO-8601 UTC (`2026-08-26T10:15:00Z`) — yalnızca bilgi amaçlı. **Yalnızca `custom/` ve `crud/` dosyalarında vardır**; wizard seed dosyasında karşılığı `Wizard.SeededAt` alanıdır |
|
| `GeneratedAt` | ISO-8601 UTC (`2026-08-26T10:15:00Z`) — yalnızca bilgi amaçlı. **Yalnızca `crud/` ve `data/` dosyalarında vardır**; wizard seed dosyasında karşılığı `Wizard.SeededAt` alanıdır |
|
||||||
| İç içe JSON | `EditorOptions`, `EditorScript`, `LookupQuery` → JSON **string** olarak kaçışlanır |
|
| İç içe JSON | `EditorOptions`, `EditorScript`, `LookupQuery` → JSON **string** olarak kaçışlanır |
|
||||||
| Satır sonu | LF |
|
| Satır sonu | LF |
|
||||||
|
|
||||||
|
|
@ -1994,20 +1995,26 @@ ListForm ile ifade edilemeyen ekranın yolu **fiziksel React sayfası değil**,
|
||||||
|
|
||||||
### 7.1 Seed dosyası
|
### 7.1 Seed dosyası
|
||||||
|
|
||||||
`configs/seeds/{kapsam}/custom/{Name}.json`
|
`configs/seeds/{kapsam}/data/App.DeveloperKit.CustomComponents.json` — Custom Components
|
||||||
|
ekranının veri dosyası. Bileşenin **kendi dosyası yoktur**; her bileşen bu dosyada bir satırdır
|
||||||
|
ve dosya `data/{ListFormCode}.json` sözleşmesini (§0.1) izler.
|
||||||
|
|
||||||
```jsonc
|
```jsonc
|
||||||
{
|
{
|
||||||
|
"ListFormCode": "App.DeveloperKit.CustomComponents",
|
||||||
|
"KeyFieldName": "Id",
|
||||||
"GeneratedAt": "2026-08-20T11:23:45Z",
|
"GeneratedAt": "2026-08-20T11:23:45Z",
|
||||||
"CustomComponents": [
|
"Order": 0,
|
||||||
|
"Rows": [
|
||||||
{
|
{
|
||||||
|
"Id": "8389ccbc-3e7b-738d-35aa-3a237eea9b9d",
|
||||||
"Name": "OrderBoard",
|
"Name": "OrderBoard",
|
||||||
"RoutePath": "/admin/order-board",
|
"RoutePath": "/admin/order-board",
|
||||||
"Code": "/*__SOZSOFT_VISUAL_DESIGNER__<url-encoded designer doc>__*/\nconst OrderBoard = () => { … }\n\nexport default OrderBoard",
|
|
||||||
"Props": "{\"visualDesigner\":{ … }}",
|
|
||||||
"Description": "",
|
"Description": "",
|
||||||
"IsActive": true,
|
"IsActive": true,
|
||||||
"Dependencies": ["OrderCard"],
|
"Dependencies": ["OrderCard"],
|
||||||
|
"Code": "/*__SOZSOFT_VISUAL_DESIGNER__<url-encoded designer doc>__*/\nconst OrderBoard = () => { … }\n\nexport default OrderBoard",
|
||||||
|
"Props": "{\"visualDesigner\":{ … }}",
|
||||||
"DataSources": [
|
"DataSources": [
|
||||||
{
|
{
|
||||||
"Name": "Mrp_T_Order — GetList",
|
"Name": "Mrp_T_Order — GetList",
|
||||||
|
|
@ -2024,6 +2031,9 @@ ListForm ile ifade edilemeyen ekranın yolu **fiziksel React sayfası değil**,
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- `Id` satırın anahtarıdır (`KeyFieldName`). Yeni bir bileşen yazarken üret; import ve seed
|
||||||
|
eşleşmesi bu değer üzerinden yapılır.
|
||||||
|
- Yeni bir bileşen dosyaya **satır olarak eklenir**; var olan satırlar korunur.
|
||||||
- `DataSources` elle yazılmaz: kaydetme sırasında `CustomComponentDataSourceResolver`
|
- `DataSources` elle yazılmaz: kaydetme sırasında `CustomComponentDataSourceResolver`
|
||||||
tasarımcı dokümanındaki `dataSources` listesini `method + path` ile CRUD endpoint kataloğunda
|
tasarımcı dokümanındaki `dataSources` listesini `method + path` ile CRUD endpoint kataloğunda
|
||||||
arayarak üretir. Eşleşenler `EntityName` + `SeedFile` taşır; elle yazılmış endpoint'ler listede
|
arayarak üretir. Eşleşenler `EntityName` + `SeedFile` taşır; elle yazılmış endpoint'ler listede
|
||||||
|
|
@ -2346,7 +2356,7 @@ Form düğümünün `id`'si) ve Save/Delete üzerinden geri yazar. Olaylar: `onL
|
||||||
- `Özel` mod: yetki adı elle yazılır.
|
- `Özel` mod: yetki adı elle yazılır.
|
||||||
- Boş bırakılan komut serbesttir.
|
- Boş bırakılan komut serbesttir.
|
||||||
|
|
||||||
Manager ekranı yetkileri: `App.DeveloperKit.Components{,.Create,.Update,.Delete}`.
|
Liste ekranı yetkileri: `App.DeveloperKit.CustomComponents{,.Create,.Update,.Delete}`.
|
||||||
|
|
||||||
### 7.6 Bileşeni menüye bağlama
|
### 7.6 Bileşeni menüye bağlama
|
||||||
|
|
||||||
|
|
@ -2397,7 +2407,7 @@ Custom yolunda `ListForm` **üretilmez**; `Title`/`Desc` dil anahtarları da ür
|
||||||
Bu çift, custom component'in menüye nasıl bağlandığının çalışan örneğidir. İncelenecek dosyalar:
|
Bu çift, custom component'in menüye nasıl bağlandığının çalışan örneğidir. İncelenecek dosyalar:
|
||||||
|
|
||||||
```
|
```
|
||||||
configs/seeds/host/custom/RoleComponent.json
|
configs/seeds/host/data/App.DeveloperKit.CustomComponents.json ← RoleComponent satırı
|
||||||
configs/seeds/host/crud/AbpRoles.json · AbpUsers.json · AbpUserRoles.json
|
configs/seeds/host/crud/AbpRoles.json · AbpUsers.json · AbpUserRoles.json
|
||||||
configs/seeds/host/wizard/RoleList.json
|
configs/seeds/host/wizard/RoleList.json
|
||||||
```
|
```
|
||||||
|
|
@ -2438,14 +2448,22 @@ butonu zip üretir:
|
||||||
|
|
||||||
```
|
```
|
||||||
wizard/{dosya}.json
|
wizard/{dosya}.json
|
||||||
custom/{component}.json (bağımlılıklarıyla)
|
data/App.DeveloperKit.CustomComponents.json (component + bağımlılıklarına süzülmüş kopya)
|
||||||
crud/{entity}.json (component'lerin kullandığı endpoint'ler)
|
crud/{entity}.json (component'lerin kullandığı endpoint'ler)
|
||||||
{sql|postgres}/{object|execute}/{nesne}.sql (List yolunda SelectCommand'ın nesnesi)
|
{sql|postgres}/{object|execute}/{nesne}.sql (List yolunda SelectCommand'ın nesnesi)
|
||||||
```
|
```
|
||||||
|
|
||||||
Import iki adımlıdır: analiz (`New`/`Identical`/`Conflict`, çakışmalar diff editörde çözülür) →
|
Import üç adımlıdır: analiz (`New`/`Identical`/`Conflict`, çakışmalar diff editörde çözülür;
|
||||||
uygulama (dosya dosya yazım, önceki hâl yedeklenir, `RollbackImport` ile toplu geri alma).
|
`data/*.json` satır bazlı karşılaştırılır — yalnızca zip'in taşıdığı anahtarlar, dolayısıyla
|
||||||
Sınırlar: yalnızca `wizard`, `crud`, `custom`, `sql`, `postgres` kök klasörleri (SQL sağlayıcı
|
ilgisiz component'ler fark olarak görünmez) →
|
||||||
|
uygulama (dosya dosya yazım, önceki hâl yedeklenir, `RollbackImport` ile toplu geri alma) →
|
||||||
|
kapanış (`CompleteImport`: yazılan `data/*.json` dosyaları `ListFormSeedDataApplier` ile
|
||||||
|
veritabanına uygulanır — tabloda olmayan satır eklenir, var olan satır dosyadaki değerlerle
|
||||||
|
güncellenir, soft delete edilmiş satırın `IsDeleted`/`DeletionTime`/`DeleterId` izleri temizlenir. Bu adım rollback kapsamında değildir; `wizard`/`crud`/`sql` dosyaları seeder ya da
|
||||||
|
deploy bekler).
|
||||||
|
`data/*.json` dosyaları kapsamdaki tüm satırları taşıdığı için üzerine yazılmaz: satırlar anahtar
|
||||||
|
alan üzerinden birleştirilir (gelen satır varsa güncellenir, yoksa eklenir, hedefteki diğer satırlar
|
||||||
|
korunur). Sınırlar: yalnızca `wizard`, `crud`, `data`, `sql`, `postgres` kök klasörleri (SQL sağlayıcı
|
||||||
klasörlerinin altında yalnızca `object`/`execute`); 5 MB/dosya, 50 MB/arşiv, 500 girdi.
|
klasörlerinin altında yalnızca `object`/`execute`); 5 MB/dosya, 50 MB/arşiv, 500 girdi.
|
||||||
Yetkiler: `App.Listforms.Wizard.Export` / `.Import`.
|
Yetkiler: `App.Listforms.Wizard.Export` / `.Import`.
|
||||||
|
|
||||||
|
|
@ -2460,7 +2478,7 @@ ref'iyle üretildiyse geri okuma da onunla yapılır.
|
||||||
|
|
||||||
Script'ler `Props.visualDesigner` içinde saklanır ve kod üreticisi tarafından tek argümanlı
|
Script'ler `Props.visualDesigner` içinde saklanır ve kod üreticisi tarafından tek argümanlı
|
||||||
(`event`) handler gövdesine gömülür. Metin gömüldüğü için **her artefakt gibi seed dosyasına da
|
(`event`) handler gövdesine gömülür. Metin gömüldüğü için **her artefakt gibi seed dosyasına da
|
||||||
yazılır**; `custom/{Name}.json` güncellenmeden bırakılan bir script veritabanı sıfırlandığında
|
yazılır**; Custom Components ekranının seed verisi güncellenmeden bırakılan bir script veritabanı sıfırlandığında
|
||||||
geri gelmez.
|
geri gelmez.
|
||||||
|
|
||||||
#### Olaylar
|
#### Olaylar
|
||||||
|
|
@ -3029,7 +3047,7 @@ bağlıdır; hiçbiri "kod yaz" adımının yerine geçmez, hepsi ondan öncedir
|
||||||
| **SQL Query Manager** | `/admin/sqlQueryManager` | `App.SqlQueryManager` | SQL nesneleri, CRUD endpoint'ler | 9.5 |
|
| **SQL Query Manager** | `/admin/sqlQueryManager` | `App.SqlQueryManager` | SQL nesneleri, CRUD endpoint'ler | 9.5 |
|
||||||
| **Custom Endpoints** | `/admin/list/App.DeveloperKit.CustomEndpoints` | `App.DeveloperKit.CustomEndpoints` | Elle yazılmış SQL'den REST endpoint | 9.2 |
|
| **Custom Endpoints** | `/admin/list/App.DeveloperKit.CustomEndpoints` | `App.DeveloperKit.CustomEndpoints` | Elle yazılmış SQL'den REST endpoint | 9.2 |
|
||||||
| **Dynamic Services** | `/admin/list/App.DeveloperKit.DynamicServices` | `App.DeveloperKit.DynamicServices` | Runtime derlenen C# AppService | 9.3 |
|
| **Dynamic Services** | `/admin/list/App.DeveloperKit.DynamicServices` | `App.DeveloperKit.DynamicServices` | Runtime derlenen C# AppService | 9.3 |
|
||||||
| **Components** | `/admin/developerkit/components` | `App.DeveloperKit.Components` | Custom Component + Visual Designer | 7 |
|
| **Components** | `/admin/list/App.DeveloperKit.CustomComponents` | `App.DeveloperKit.CustomComponents` | Custom Component + Visual Designer | 7 |
|
||||||
| **ListForm** | `/admin/list/App.Listforms.Listform` | `App.Listforms.Listform` | Var olan ekranların ham tanımı | 9.6 |
|
| **ListForm** | `/admin/list/App.Listforms.Listform` | `App.Listforms.Listform` | Var olan ekranların ham tanımı | 9.6 |
|
||||||
| **Wizard Manager** | `/admin/listform/wizardManager` | `App.Listforms.Wizard` | Wizard seed dosyalarının yönetimi, export/import | 4, 7.7 |
|
| **Wizard Manager** | `/admin/listform/wizardManager` | `App.Listforms.Wizard` | Wizard seed dosyalarının yönetimi, export/import | 4, 7.7 |
|
||||||
|
|
||||||
|
|
@ -3502,13 +3520,13 @@ Kurallar:
|
||||||
### 10.3 "Serbest yerleşimli sayfa / dashboard"
|
### 10.3 "Serbest yerleşimli sayfa / dashboard"
|
||||||
|
|
||||||
1. Gerekli endpoint'ler yoksa CRUD Endpoint üret.
|
1. Gerekli endpoint'ler yoksa CRUD Endpoint üret.
|
||||||
2. Component Manager → yeni bileşen → Visual Designer:
|
2. Custom Components ekranı → yeni satır → **Design** → Visual Designer:
|
||||||
`PageContainer` → `FlexRow` → içine `platform` düğümleri (`listFormCode` ile) ve/veya `Form`.
|
`PageContainer` → `FlexRow` → içine `platform` düğümleri (`listFormCode` ile) ve/veya `Form`.
|
||||||
3. Veri kaynaklarını Data sekmesinden bağla; filtreleri `route`/`query`/`record` kaynaklarıyla
|
3. Veri kaynaklarını Data sekmesinden bağla; filtreleri `route`/`query`/`record` kaynaklarıyla
|
||||||
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: `custom/{Name}.json` ve ilgili `crud/*.json` dosyaları oluştu mu.
|
6. Doğrula: `data/App.DeveloperKit.CustomComponents.json` içindeki satır ve ilgili `crud/*.json` dosyaları oluştu mu.
|
||||||
|
|
||||||
### 10.4 "Kanban panosu"
|
### 10.4 "Kanban panosu"
|
||||||
|
|
||||||
|
|
@ -3554,13 +3572,13 @@ Bu tür isteklerin tamamı aynı iskeleti kullanır. Kullanıcı "avans talep fo
|
||||||
configs/seeds/host/
|
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
|
||||||
├── custom/AdvanceRequestForm.json ← yalnızca serbest yerleşim isteniyorsa
|
├── data/App.DeveloperKit.CustomComponents.json ← yalnızca serbest yerleşim isteniyorsa
|
||||||
├── wizard/AdvanceRequestApprovals.json ← (varsa) menüsüz alt ekran, küçük SeededAt
|
├── wizard/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ışı
|
||||||
```
|
```
|
||||||
|
|
||||||
**Karar:** Talep formu **standart bir kayıt listesi + düzenleme formu + onay akışı** ise
|
**Karar:** Talep formu **standart bir kayıt listesi + düzenleme formu + onay akışı** ise
|
||||||
`ComponentKind: 0` (List) yeter ve `custom/` + `crud/` dosyalarına hiç gerek yoktur. Custom
|
`ComponentKind: 0` (List) yeter ve component seed verisi + `crud/` dosyalarına hiç gerek yoktur. Custom
|
||||||
Component yoluna yalnızca serbest yerleşim (özet kartlar, çok adımlı sihirbaz, gömülü panolar)
|
Component yoluna yalnızca serbest yerleşim (özet kartlar, çok adımlı sihirbaz, gömülü panolar)
|
||||||
isteniyorsa geçilir.
|
isteniyorsa geçilir.
|
||||||
|
|
||||||
|
|
|
||||||
82
README.md
82
README.md
|
|
@ -245,17 +245,17 @@ runtime'da düzenlenebilen seed'ler `configs/seeds/` altındadır — bkz. `App:
|
||||||
|
|
||||||
`configs/seeds` altında en üst seviye kapsamdır (CDN klasörleriyle aynı mantık): host
|
`configs/seeds` altında en üst seviye kapsamdır (CDN klasörleriyle aynı mantık): host
|
||||||
veritabanı için `host/`, her tenant için `tenants/{tenantId}/`. Kapsamın altında içerik
|
veritabanı için `host/`, her tenant için `tenants/{tenantId}/`. Kapsamın altında içerik
|
||||||
türüne göre `crud/`, `custom/`, `wizard/`, `data/`, `sql/`, `postgres/` klasörleri bulunur:
|
türüne göre `crud/`, `wizard/`, `data/`, `sql/`, `postgres/` klasörleri bulunur:
|
||||||
|
|
||||||
```
|
```
|
||||||
configs/seeds/
|
configs/seeds/
|
||||||
host/
|
host/
|
||||||
crud/ custom/ wizard/ data/
|
crud/ wizard/ data/
|
||||||
sql/ { object/ execute/ }
|
sql/ { object/ execute/ }
|
||||||
postgres/ { object/ execute/ }
|
postgres/ { object/ execute/ }
|
||||||
tenants/
|
tenants/
|
||||||
{tenantId}/
|
{tenantId}/
|
||||||
crud/ custom/ wizard/ data/
|
crud/ wizard/ data/
|
||||||
sql/ { object/ execute/ }
|
sql/ { object/ execute/ }
|
||||||
postgres/ { object/ execute/ }
|
postgres/ { object/ execute/ }
|
||||||
```
|
```
|
||||||
|
|
@ -269,8 +269,7 @@ Aktif veritabanı sağlayıcısına göre `sql/` (SQL Server) ya da `postgres/`
|
||||||
| `{sql\|postgres}/execute` | Nesneyi oluşturur; ayrıca tüm migration ve seed'ler bittikten sonra `AfterAllMigrationsSqlExecutor` dosya adından türettiği procedure'ü çalıştırır (dosya adı = procedure adı, `schema.Procedure.sql` de olur). |
|
| `{sql\|postgres}/execute` | Nesneyi oluşturur; ayrıca tüm migration ve seed'ler bittikten sonra `AfterAllMigrationsSqlExecutor` dosya adından türettiği procedure'ü çalıştırır (dosya adı = procedure adı, `schema.Procedure.sql` de olur). |
|
||||||
| `wizard` | ListForm Wizard'ın ürettiği `.json` konfigürasyonları; `WizardDataSeeder` kendi kapsamındaki dosyaları okur. |
|
| `wizard` | ListForm Wizard'ın ürettiği `.json` konfigürasyonları; `WizardDataSeeder` kendi kapsamındaki dosyaları okur. |
|
||||||
| `crud` | CrudEndpoint konfigürasyonları; `CrudDataSeeder` kendi kapsamındaki dosyaları okur. |
|
| `crud` | CrudEndpoint konfigürasyonları; `CrudDataSeeder` kendi kapsamındaki dosyaları okur. |
|
||||||
| `custom` | Custom Component konfigürasyonları (`{Name}.json`); Component Manager kaydetme/silme sırasında dosyayı senkronlar, `CustomComponentDataSeeder` kendi kapsamındaki dosyaları okuyup veritabanına uygular. |
|
| `data` | Ekranlardan girilen listelerin veri aynası (`{ListFormCode}.json`). `ListForm.SeedFilePath` dolu olan ekranda `SeedSyncInsert/Update/Delete` bayraklarından işaretli olan işlemler dosyaya yansır (`ListFormSeedDataSynchronizer`); migrate/seed sırasında bütün contributor'lar bittikten sonra `ListFormSeedDataApplier` dosyayı geri uygular: anahtarı veritabanında **olmayan** satırları ekler, var olanlara dokunmaz. 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ı (`{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. |
|
|
||||||
|
|
||||||
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.
|
||||||
|
|
@ -554,8 +553,10 @@ Yetkiler, Alt Form, Pivot, Tree, Gantt, Scheduler, Todo, Widget, Workflow, Durum
|
||||||
**Wizard File Manager** (`/admin/listform/wizard` liste ekranı) üretilmiş seed dosyalarını yönetir:
|
**Wizard File Manager** (`/admin/listform/wizard` liste ekranı) üretilmiş seed dosyalarını yönetir:
|
||||||
liste/kart görünümü arasında geçiş (tercih diğer listelerle aynı yerde, `admin.lists.states`
|
liste/kart görünümü arasında geçiş (tercih diğer listelerle aynı yerde, `admin.lists.states`
|
||||||
altında saklanır), arama, düzenleme (dosyanın yerine geçen yeni bir çalıştırma), silme, veritabanı
|
altında saklanır), arama, düzenleme (dosyanın yerine geçen yeni bir çalıştırma), silme, veritabanı
|
||||||
migrate tetikleme, dışa aktarma ve içe aktarma. Butonlar `App.Listforms.Wizard.*` yetkileriyle
|
migrate tetikleme, dışa aktarma ve içe aktarma. Liste, wizard'ın ilk deploy damgasına
|
||||||
gizlenir; asıl kontrol `ListFormWizardAppService` üzerindedir.
|
(`Wizard.SeededAt`) göre **tersine** sıralıdır — en son üretilen ekran en üstte durur; damgası
|
||||||
|
okunamayan dosyalar sona düşer. Butonlar `App.Listforms.Wizard.*` yetkileriyle gizlenir; asıl
|
||||||
|
kontrol `ListFormWizardAppService` üzerindedir.
|
||||||
|
|
||||||
### 7.7. İçe/dışa aktarma
|
### 7.7. İçe/dışa aktarma
|
||||||
|
|
||||||
|
|
@ -575,21 +576,46 @@ aynı klasör düzenine doğrudan açılabilir.
|
||||||
| Yol | İçerik |
|
| Yol | İçerik |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `wizard/{dosya}.json` | Wizard tanımının kendisi |
|
| `wizard/{dosya}.json` | Wizard tanımının kendisi |
|
||||||
| `custom/{component}.json` | Custom yolunda bağlı component ve bağımlılıkları |
|
| `data/App.DeveloperKit.CustomComponents.json` | Custom yolunda bağlı component ve bağımlılıkları — dosyanın yalnızca bu satırlara süzülmüş kopyası |
|
||||||
| `crud/{entity}.json` | Component'lerin/nesnenin kullandığı CRUD endpoint tanımları |
|
| `crud/{entity}.json` | Component'lerin/nesnenin kullandığı CRUD endpoint tanımları |
|
||||||
| `{sql\|postgres}/{object\|execute}/{nesne}.sql` | List yolunda `SelectCommand`'a karşılık gelen SQL nesnesi |
|
| `{sql\|postgres}/{object\|execute}/{nesne}.sql` | List yolunda `SelectCommand`'a karşılık gelen SQL nesnesi |
|
||||||
|
|
||||||
**İçe aktarma iki adımlıdır** (`WizardImportDialog`):
|
**İçe aktarma üç adımlıdır** (`WizardImportDialog`):
|
||||||
|
|
||||||
1. **Analiz** (`AnalyzeImport`) — zip, seed kökü altındaki `.imports/{importId}/staged` klasörüne
|
1. **Analiz** (`AnalyzeImport`) — zip, seed kökü altındaki `.imports/{importId}/staged` klasörüne
|
||||||
açılır (nokta ile başladığı için seeder taramalarına girmez) ve her dosya hedefteki karşılığıyla
|
açılır (nokta ile başladığı için seeder taramalarına girmez) ve her dosya hedefteki karşılığıyla
|
||||||
karşılaştırılır: `New` (hedefte yok), `Identical` (aynı, sorulmaz), `Conflict` (farklı).
|
karşılaştırılır: `New` (hedefte yok), `Identical` (aynı, sorulmaz), `Conflict` (farklı).
|
||||||
Çakışan dosyalar Monaco `DiffEditor` ile karşılaştırılıp birleştirilir.
|
Çakışan dosyalar Monaco `DiffEditor` ile karşılaştırılıp birleştirilir.
|
||||||
|
`data/*.json` girdilerinde karşılaştırma **satır bazlıdır**: hedef dosyadan yalnızca gelen
|
||||||
|
satırların anahtarları süzülür, dosya düzeyindeki `GeneratedAt`/`Order` karşılaştırmaya girmez.
|
||||||
|
Böylece aktarılan kaydın dışındaki satırlar fark olarak görünmez; gelen satırların hiçbiri
|
||||||
|
hedefte yoksa dosya var olsa bile durum `New` (ekleme), varsa ve içerik aynıysa `Identical`
|
||||||
|
(hiç aktarılmaz), farklıysa `Conflict` (güncelleme) olur.
|
||||||
2. **Uygulama** (`ApplyImport`) — dosyalar tek tek yazılır, ilerleme satır satır gösterilir. Yazılan
|
2. **Uygulama** (`ApplyImport`) — dosyalar tek tek yazılır, ilerleme satır satır gösterilir. Yazılan
|
||||||
her dosyanın önceki hâli `.imports/{importId}/backup` altına alındığı için hata durumunda
|
her dosyanın önceki hâli `.imports/{importId}/backup` altına alındığı için hata durumunda
|
||||||
`RollbackImport` ile toplu işlem geri alınabilir; `CompleteImport` oturumu kapatır.
|
`RollbackImport` ile toplu işlem geri alınabilir.
|
||||||
|
3. **Kapanış** (`CompleteImport`) — oturum kapatılmadan önce zip'in getirdiği `data/*.json` dosyaları
|
||||||
|
`IListFormSeedDataApplier.ApplyFilesAsync(..., overwriteExisting: true)` ile veritabanına
|
||||||
|
uygulanır: tabloda olmayan satır eklenir, var olan satır dosyadaki değerlerle **güncellenir**
|
||||||
|
(migrate/seed akışından farkı budur — orada dosya bayat olabileceği için canlı kayda
|
||||||
|
dokunulmaz). Kayıt daha önce **soft delete** edilmişse silinme izleri de temizlenir
|
||||||
|
(`IsDeleted`, `DeletionTime`, `DeleterId`); aksi halde satır tabloda durur ama ekranın
|
||||||
|
varsayılan süzgeci (`"IsDeleted" = 'false'`) onu gizlemeye devam ederdi. Hangi kolonların
|
||||||
|
sıfırlanacağı ekranın `DeleteCommand`'ından okunur, hard delete yapan ekranda hiçbir şey
|
||||||
|
eklenmez. Hedefle **zaten aynı** olan (`Identical`) veri dosyaları da uygulanır: yazacak bir şey
|
||||||
|
yoktur ama satırlar veritabanında eksik ya da silinmiş olabilir. Kullanıcının "mevcudu koru"
|
||||||
|
dediği (farklı olup yazılmayan) dosyaya dokunulmaz. Anahtarı olmayan satır güncellenemez,
|
||||||
|
atlanır; eklenen/güncellenen/atlanan satır
|
||||||
|
sayısı dialogda raporlanır. Bu adım `RollbackImport`
|
||||||
|
kapsamında **değildir** — geri alma bu adımdan önce yapılır. Diğer klasörler (`wizard`, `crud`,
|
||||||
|
`sql`) dosya olarak kalır; veritabanına seeder ya da wizard deploy ile geçer.
|
||||||
|
|
||||||
Güvenlik sınırları: yalnızca `wizard`, `crud`, `custom`, `sql`, `postgres` kök klasörleri
|
`data/*.json` dosyaları kapsamdaki bütün satırları taşıdığı için üzerine yazılmaz: satırlar anahtar
|
||||||
|
alan üzerinden birleştirilir (gelen satır varsa günceller, yoksa eklenir, hedefteki diğer satırlar
|
||||||
|
korunur). Diff ekranında düzenlenen içerik de aynı sözleşmeyi taşıdığı için elle birleştirme bu
|
||||||
|
davranışı bozmaz.
|
||||||
|
|
||||||
|
Güvenlik sınırları: yalnızca `wizard`, `crud`, `data`, `sql`, `postgres` kök klasörleri
|
||||||
(SQL sağlayıcı klasörlerinin altında yalnızca `object` ve `execute`) kabul edilir; dosya başına
|
(SQL sağlayıcı klasörlerinin altında yalnızca `object` ve `execute`) kabul edilir; dosya başına
|
||||||
5 MB, arşiv başına 50 MB, en çok 500 girdi. Kabul edilmeyen girdiler analiz sonucunda uyarı
|
5 MB, arşiv başına 50 MB, en çok 500 girdi. Kabul edilmeyen girdiler analiz sonucunda uyarı
|
||||||
olarak listelenir. Yetkiler: `App.Listforms.Wizard.Export` / `App.Listforms.Wizard.Import`.
|
olarak listelenir. Yetkiler: `App.Listforms.Wizard.Export` / `App.Listforms.Wizard.Import`.
|
||||||
|
|
@ -606,7 +632,7 @@ Teknik kullanıcılar için `/admin/developerkit` altında toplanan araçlar:
|
||||||
| **Custom Endpoint** | `/admin/list/App.DeveloperKit.CustomEndpoints` | SQL veya servis tanımından REST endpoint üretimi; GET/POST/PUT/DELETE için ayrı yetki kapıları (`App.DeveloperKit.CustomEndpoints.*`) ve kullanıcı/rol/global erişim kuralları. |
|
| **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/developerkit/components` | React bileşenini veritabanında saklama; `@babel/standalone` ile tarayıcıda derleyip route'a bağlama. Liste/kart görünümü, arama; yetkiler `App.DeveloperKit.Components{,.Create,.Update,.Delete}`. |
|
| **Custom Component** | `/admin/list/App.DeveloperKit.CustomComponents` | React bileşenini veritabanında saklama; `@babel/standalone` ile tarayıcıda derleyip route'a bağlama. Liste bir ListForm ekranıdır (KPI şeridi, ızgara/kart görünümü, grid'in kendi ekleme/düzenleme formu); satırdaki **Design** butonu `/admin/developerkit/components/design/{id}` tasarımcısını, **Open URL** butonu bileşenin `RoutePath`'ini açar. Kayıtlar `configs/seeds/host/data/App.DeveloperKit.CustomComponents.json` dosyasına senkronlanır. Yetkiler `App.DeveloperKit.CustomComponents{,.Create,.Update,.Delete}`. |
|
||||||
| **Visual Designer** | Component editörü içinde | Sürükle-bırak kanvas ile bileşen üretimi ve kod üretimi (`visualDesigner/codeGenerator.ts`). |
|
| **Visual Designer** | Component editörü içinde | Sürükle-bırak kanvas ile bileşen üretimi ve kod üretimi (`visualDesigner/codeGenerator.ts`). |
|
||||||
| **ListForm** | `/admin/list/App.Listforms.Listform` | Var olan ekranların ham tanım listesi; buradan `/admin/listform/edit/{kod}` sekmeli editörüne geçilir. |
|
| **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}`. |
|
||||||
|
|
@ -665,16 +691,16 @@ satırındaki `/*__SOZSOFT_VISUAL_DESIGNER__…__*/` yorumunda. Böylece yalnız
|
||||||
tasarımcı dokümanı geri okunabilir. `sourceMode` alanı `visual` ya da `code` olur; kod moduna
|
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.** Component Manager üzerinden kaydedilen/silinen her bileşen
|
**Seed senkronizasyonu.** Bileşenlerin seed kaynağı Custom Components ekranının veri dosyasıdır:
|
||||||
`configs/seeds/{host|tenants/{tenantId}}/custom/{Name}.json` olarak yazılır;
|
`configs/seeds/{host|tenants/{tenantId}}/data/App.DeveloperKit.CustomComponents.json`. Bileşene
|
||||||
`CustomComponentDataSeeder` veritabanı silinip yeniden oluşturulduğunda aynı dosyaları okuyup geri
|
yazan **iki yol** da bu dosyaya işlenir (`ListFormSeedDataSynchronizer`): listeden yapılan
|
||||||
yükler. Dosya düzeni bilinçli olarak `TenantData.json` içindeki `CustomComponents` bloğuyla
|
ekleme/güncelleme/silme ve Visual Designer'ın kaydı (`CustomComponentAppService`). Veritabanı
|
||||||
aynıdır. `DataSources` sütunu tasarımcı dokümanından türetilir
|
sıfırlandığında `ListFormSeedDataApplier` satırları geri yazar; `Code`, `Props` ve `DataSources`
|
||||||
(`CustomComponentDataSourceResolver`): Data sekmesindeki her endpoint `method + path` ile CRUD
|
kolonları bu yüzden ekranda gizli alan olarak tanımlıdır. `DataSources` sütunu tasarımcı
|
||||||
endpoint kataloğunda aranır, eşleşenler `EntityName` + `crud/{EntityName}.json` referansını taşır,
|
dokümanından türetilir (`CustomComponentDataSourceResolver`): Data sekmesindeki her endpoint
|
||||||
elle yazılmış olanlar listede kalır ama bu alanları boş gelir. Aynı çözüm hem kaydetmede (katalog
|
`method + path` ile CRUD endpoint kataloğunda aranır, eşleşenler `EntityName` +
|
||||||
veritabanından) hem seed'de (katalog `crud/*.json` dosyalarından) kullanıldığı için sütun iki
|
`crud/{EntityName}.json` referansını taşır, elle yazılmış olanlar listede kalır ama bu alanları
|
||||||
yolda da aynı üretilir.
|
boş gelir. Wizard export'u bu referansları izleyerek bağlı `crud/*.json` dosyalarını zip'e ekler.
|
||||||
|
|
||||||
**Toolbox.** Kanvasa bırakılabilecekler beş aileye ayrılır:
|
**Toolbox.** Kanvasa bırakılabilecekler beş aileye ayrılır:
|
||||||
|
|
||||||
|
|
@ -715,7 +741,8 @@ parametresi (`?RoleId=…`), diğerleri son ek taşır (`?Name.contains=…`). O
|
||||||
**Diğer notlar.** `PlatformIcon` tasarımcı dokümanındaki ikon adını çözer; `selectComponents.ts`
|
**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/custom/NewComponent.json` şablonundan başlar.
|
nokta budur. Yeni bileşen `configs/seeds/host/data/App.DeveloperKit.CustomComponents.json`
|
||||||
|
içindeki bir satır olarak başlar.
|
||||||
|
|
||||||
**Dinamik servis yaşam döngüsü:** `DynamicServiceCompiler` kodu derler →
|
**Dinamik servis yaşam döngüsü:** `DynamicServiceCompiler` kodu derler →
|
||||||
`DynamicAssemblyRegistrationService` assembly'yi tenant bağlamıyla kaydeder →
|
`DynamicAssemblyRegistrationService` assembly'yi tenant bağlamıyla kaydeder →
|
||||||
|
|
@ -756,7 +783,7 @@ içindedir; oradaki kontroller yalnızca butonları gizler, asıl kontrol AppSer
|
||||||
| Grup | Alt yetkiler |
|
| Grup | Alt yetkiler |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `App.Listforms.Wizard` | `.Create`, `.Update`, `.Delete`, `.Export`, `.Import` |
|
| `App.Listforms.Wizard` | `.Create`, `.Update`, `.Delete`, `.Export`, `.Import` |
|
||||||
| `App.DeveloperKit.Components` | `.Create`, `.Update`, `.Delete` |
|
| `App.DeveloperKit.CustomComponents` | `.Create`, `.Update`, `.Delete` |
|
||||||
| `App.DeveloperKit.CustomEndpoints` | `.Get`, `.Post`, `.Put`, `.Remove` (dispatcher üzerinden çağrı kapısı; endpoint bazlı User/Role/Global kuralları bunun üstünde denetlenir) |
|
| `App.DeveloperKit.CustomEndpoints` | `.Get`, `.Post`, `.Put`, `.Remove` (dispatcher üzerinden çağrı kapısı; endpoint bazlı User/Role/Global kuralları bunun üstünde denetlenir) |
|
||||||
| `App.DeveloperKit.DynamicServices` | `.Create`, `.Edit`, `.Delete`, `.Manage`, `.TestCompile`, `.Publish`, `.ViewCode` |
|
| `App.DeveloperKit.DynamicServices` | `.Create`, `.Edit`, `.Delete`, `.Manage`, `.TestCompile`, `.Publish`, `.ViewCode` |
|
||||||
| `App.SqlQueryManager` | `.CrudEndpoints` |
|
| `App.SqlQueryManager` | `.CrudEndpoints` |
|
||||||
|
|
@ -1168,18 +1195,17 @@ derlenip yayınlanır. Yeni bir modülün nasıl kurulacağını öğrenmek içi
|
||||||
- `PermissionsData.json` — yetki grupları ve tanımları
|
- `PermissionsData.json` — yetki grupları ve tanımları
|
||||||
- `HostData.json` — ayar/entegrasyon değerleri
|
- `HostData.json` — ayar/entegrasyon değerleri
|
||||||
- `LanguagesData.json` — dil anahtarları (EN/TR)
|
- `LanguagesData.json` — dil anahtarları (EN/TR)
|
||||||
- `WizardDataSeeder.cs`, `CustomComponentDataSeeder.cs`, `CrudDataSeeder.cs` — aşağıdaki
|
- `WizardDataSeeder.cs`, `CrudDataSeeder.cs` — aşağıdaki
|
||||||
runtime seed dosyalarını okuyup uygulayan seeder'lar
|
runtime seed dosyalarını okuyup uygulayan seeder'lar
|
||||||
|
|
||||||
**2. Runtime'da üretilen seed'ler** — `configs/seeds/`. Wizard, Component Manager ve CRUD Endpoint
|
**2. Runtime'da üretilen seed'ler** — `configs/seeds/`. Wizard, Custom Components ekranı/Visual
|
||||||
Manager çalışma zamanında burayı yazar; veritabanı silinip yeniden oluşturulduğunda aynı dosyalar
|
Designer ve CRUD Endpoint Manager çalışma zamanında burayı yazar; veritabanı silinip yeniden oluşturulduğunda aynı dosyalar
|
||||||
geri yüklenir. Kapsam klasörü CDN düzeniyle aynıdır ve `SeedPathResolver` üzerinden çözülür:
|
geri yüklenir. Kapsam klasörü CDN düzeniyle aynıdır ve `SeedPathResolver` üzerinden çözülür:
|
||||||
|
|
||||||
```
|
```
|
||||||
configs/seeds/
|
configs/seeds/
|
||||||
├── host/ # host kapsamı
|
├── host/ # host kapsamı
|
||||||
│ ├── wizard/ # {Ad}.json → WizardDataSeeder
|
│ ├── wizard/ # {Ad}.json → WizardDataSeeder
|
||||||
│ ├── custom/ # {ComponentName}.json → CustomComponentDataSeeder
|
|
||||||
│ ├── crud/ # {EntityName}.json → CrudDataSeeder
|
│ ├── crud/ # {EntityName}.json → CrudDataSeeder
|
||||||
│ ├── sql/{object,execute}/ # .sql (SQL Server)
|
│ ├── sql/{object,execute}/ # .sql (SQL Server)
|
||||||
│ └── postgres/{object,execute}/
|
│ └── postgres/{object,execute}/
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Volo.Abp.Application.Dtos;
|
using Volo.Abp.Application.Dtos;
|
||||||
using Volo.Abp.Application.Services;
|
using Volo.Abp.Application.Services;
|
||||||
|
|
||||||
|
|
@ -12,7 +10,4 @@ public interface ICustomComponentAppService : ICrudAppService<
|
||||||
PagedAndSortedResultRequestDto,
|
PagedAndSortedResultRequestDto,
|
||||||
CreateUpdateCustomComponentDto>
|
CreateUpdateCustomComponentDto>
|
||||||
{
|
{
|
||||||
Task<List<CustomComponentDto>> GetActiveComponentsAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ public interface IListFormWizardAppService
|
||||||
Task<WizardImportApplyResultDto> ApplyImport(WizardImportApplyInput input);
|
Task<WizardImportApplyResultDto> ApplyImport(WizardImportApplyInput input);
|
||||||
|
|
||||||
/// <summary>Import oturumunu basariyla kapatir ve staging/yedek klasorunu siler.</summary>
|
/// <summary>Import oturumunu basariyla kapatir ve staging/yedek klasorunu siler.</summary>
|
||||||
Task CompleteImport(string importId);
|
Task<WizardImportCompleteResultDto> CompleteImport(string importId);
|
||||||
|
|
||||||
/// <summary>O ana kadar yazilan dosyalari geri alir ve staging klasorunu siler.</summary>
|
/// <summary>O ana kadar yazilan dosyalari geri alir ve staging klasorunu siler.</summary>
|
||||||
Task RollbackImport(string importId);
|
Task RollbackImport(string importId);
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,10 @@ namespace Sozsoft.Platform.ListForms;
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class WizardImportEntryDto
|
public class WizardImportEntryDto
|
||||||
{
|
{
|
||||||
/// <summary>Kapsam klasorune goreli yol, ornegin <c>custom/Orders.json</c>.</summary>
|
/// <summary>Kapsam klasorune goreli yol, ornegin <c>crud/Orders.json</c>.</summary>
|
||||||
public string Path { get; set; }
|
public string Path { get; set; }
|
||||||
|
|
||||||
/// <summary>Yolun ilk segmenti; listede gruplama icin kullanilir (wizard, crud, custom, sql, postgres).</summary>
|
/// <summary>Yolun ilk segmenti; listede gruplama icin kullanilir (wizard, crud, data, sql, postgres).</summary>
|
||||||
public string Folder { get; set; }
|
public string Folder { get; set; }
|
||||||
|
|
||||||
public string FileName { get; set; }
|
public string FileName { get; set; }
|
||||||
|
|
@ -70,3 +70,31 @@ public class WizardImportApplyResultDto
|
||||||
|
|
||||||
public long Size { get; set; }
|
public long Size { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Import kapanisinin sonucu. Yazilan <c>data/*.json</c> dosyalari ayni islemde veritabanina
|
||||||
|
/// da uygulanir; her dosyanin sonucu burada raporlanir.
|
||||||
|
/// </summary>
|
||||||
|
public class WizardImportCompleteResultDto
|
||||||
|
{
|
||||||
|
public List<WizardImportSeedApplyResultDto> AppliedData { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Tek bir veri seed dosyasinin veritabanina uygulanma sonucu.</summary>
|
||||||
|
public class WizardImportSeedApplyResultDto
|
||||||
|
{
|
||||||
|
/// <summary>Kapsam klasorune goreli yol.</summary>
|
||||||
|
public string Path { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Veritabanina eklenen satir sayisi.</summary>
|
||||||
|
public int InsertedRowCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Zaten var oldugu icin dosyadaki degerlerle guncellenen satir sayisi.</summary>
|
||||||
|
public int UpdatedRowCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Anahtari olmadigi icin guncellenemeyen, dokunulmayan satir sayisi.</summary>
|
||||||
|
public int SkippedRowCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Dosya uygulanamadiysa nedeni; basarili durumda bostur.</summary>
|
||||||
|
public string Error { get; set; }
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,15 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.Encodings.Web;
|
using System.Text.Encodings.Web;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Sozsoft.Platform.Data.Seeds;
|
using Sozsoft.Platform.Data.Seeds;
|
||||||
using Sozsoft.Platform.DeveloperKit;
|
using Sozsoft.Platform.DeveloperKit;
|
||||||
using Sozsoft.Platform.Entities;
|
using Sozsoft.Platform.Entities;
|
||||||
|
using Sozsoft.Platform.ListForms;
|
||||||
using Volo.Abp.Application.Dtos;
|
using Volo.Abp.Application.Dtos;
|
||||||
using Volo.Abp.Application.Services;
|
using Volo.Abp.Application.Services;
|
||||||
using Volo.Abp.Domain.Repositories;
|
using Volo.Abp.Domain.Repositories;
|
||||||
|
|
@ -21,9 +20,13 @@ namespace Platform.Api.Application;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Custom component yonetimi. Sinif seviyesindeki <see cref="AuthorizeAttribute"/> yalnizca
|
/// Custom component yonetimi. Sinif seviyesindeki <see cref="AuthorizeAttribute"/> yalnizca
|
||||||
/// kimlik dogrulamasi ister; ekrani gorme ve CRUD haklari asagida ABP policy adlarina baglanir.
|
/// kimlik dogrulamasi ister; ekrani gorme ve CRUD haklari asagida ABP policy adlarina baglanir.
|
||||||
/// <c>App.DeveloperKit.Components</c> okuma, <c>.Create</c>/<c>.Update</c>/<c>.Delete</c> aksiyon
|
/// <c>App.DeveloperKit.CustomComponents</c> okuma, <c>.Create</c>/<c>.Update</c>/<c>.Delete</c> aksiyon
|
||||||
/// yetkileridir. <see cref="GetActiveComponentsAsync"/> calisma zamani okuma yoludur ve yalnizca
|
/// yetkileridir.
|
||||||
/// oturum acmis olmayi ister.
|
/// <para>
|
||||||
|
/// Component'ler <c>App.DeveloperKit.CustomComponents</c> ekraninin verisidir; ekran uzerinden
|
||||||
|
/// yapilan kayitlar seed dosyasina ListForm altyapisi tarafindan yansitilir. Designer bu servisi
|
||||||
|
/// kullandigi icin ayni yansitma burada da tetiklenir, aksi halde dosya bayat kalir.
|
||||||
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Authorize]
|
[Authorize]
|
||||||
public class CustomComponentAppService : CrudAppService<
|
public class CustomComponentAppService : CrudAppService<
|
||||||
|
|
@ -40,21 +43,19 @@ public class CustomComponentAppService : CrudAppService<
|
||||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
||||||
};
|
};
|
||||||
|
|
||||||
private static readonly JsonSerializerOptions JsonReadOptions = new()
|
/// <summary>Component'lerin verisini tasiyan ekran; seed dosyasi bu kod uzerinden bulunur.</summary>
|
||||||
{
|
private const string ListFormCode = SeedConsts.AppCodes.DeveloperKits.CustomComponents;
|
||||||
PropertyNameCaseInsensitive = true
|
|
||||||
};
|
|
||||||
|
|
||||||
private readonly IConfiguration _configuration;
|
|
||||||
private readonly IRepository<CrudEndpoint, Guid> _crudEndpointRepository;
|
private readonly IRepository<CrudEndpoint, Guid> _crudEndpointRepository;
|
||||||
|
private readonly IListFormSeedDataSynchronizer _seedDataSynchronizer;
|
||||||
|
|
||||||
public CustomComponentAppService(
|
public CustomComponentAppService(
|
||||||
IRepository<CustomComponent, Guid> repository,
|
IRepository<CustomComponent, Guid> repository,
|
||||||
IRepository<CrudEndpoint, Guid> crudEndpointRepository,
|
IRepository<CrudEndpoint, Guid> crudEndpointRepository,
|
||||||
IConfiguration configuration) : base(repository)
|
IListFormSeedDataSynchronizer seedDataSynchronizer) : base(repository)
|
||||||
{
|
{
|
||||||
_crudEndpointRepository = crudEndpointRepository;
|
_crudEndpointRepository = crudEndpointRepository;
|
||||||
_configuration = configuration;
|
_seedDataSynchronizer = seedDataSynchronizer;
|
||||||
|
|
||||||
GetPolicyName = AppCodes.DeveloperKits.Components.Component;
|
GetPolicyName = AppCodes.DeveloperKits.Components.Component;
|
||||||
GetListPolicyName = AppCodes.DeveloperKits.Components.Component;
|
GetListPolicyName = AppCodes.DeveloperKits.Components.Component;
|
||||||
|
|
@ -67,7 +68,7 @@ public class CustomComponentAppService : CrudAppService<
|
||||||
{
|
{
|
||||||
var result = await base.CreateAsync(input);
|
var result = await base.CreateAsync(input);
|
||||||
await SyncDataSourcesAsync(result.Id);
|
await SyncDataSourcesAsync(result.Id);
|
||||||
await SyncSeedFileAsync(result.Id);
|
await _seedDataSynchronizer.SyncRowAsync(ListFormCode, result.Id, SeedSyncOperation.Insert);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -76,7 +77,6 @@ public class CustomComponentAppService : CrudAppService<
|
||||||
CreateUpdateCustomComponentDto input)
|
CreateUpdateCustomComponentDto input)
|
||||||
{
|
{
|
||||||
var existingComponent = await Repository.GetAsync(id);
|
var existingComponent = await Repository.GetAsync(id);
|
||||||
var previousName = existingComponent.Name;
|
|
||||||
|
|
||||||
if (input.Props is null)
|
if (input.Props is null)
|
||||||
{
|
{
|
||||||
|
|
@ -85,109 +85,15 @@ public class CustomComponentAppService : CrudAppService<
|
||||||
|
|
||||||
var result = await base.UpdateAsync(id, input);
|
var result = await base.UpdateAsync(id, input);
|
||||||
|
|
||||||
// Ad degistiyse eski dosya artik hicbir component'i temsil etmez.
|
|
||||||
if (!string.Equals(previousName, result.Name, StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
DeleteSeedFile(previousName);
|
|
||||||
}
|
|
||||||
|
|
||||||
await SyncDataSourcesAsync(id);
|
await SyncDataSourcesAsync(id);
|
||||||
await SyncSeedFileAsync(id);
|
await _seedDataSynchronizer.SyncRowAsync(ListFormCode, id, SeedSyncOperation.Update);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task DeleteAsync(Guid id)
|
public override async Task DeleteAsync(Guid id)
|
||||||
{
|
{
|
||||||
var component = await Repository.FindAsync(id);
|
|
||||||
await base.DeleteAsync(id);
|
await base.DeleteAsync(id);
|
||||||
|
await _seedDataSynchronizer.RemoveRowsAsync(ListFormCode, [id]);
|
||||||
if (component != null)
|
|
||||||
{
|
|
||||||
DeleteSeedFile(component.Name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<List<CustomComponentDto>> GetActiveComponentsAsync()
|
|
||||||
{
|
|
||||||
var components = await Repository.GetListAsync(x => x.IsActive);
|
|
||||||
return await MapToGetListOutputDtosAsync(components);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Seed dosyasini yazar; dosya islemi hatasi CRUD islemini engellemez, yalnizca loglanir.
|
|
||||||
/// </summary>
|
|
||||||
private async Task SyncSeedFileAsync(Guid id)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var component = await Repository.FindAsync(id);
|
|
||||||
if (component != null)
|
|
||||||
{
|
|
||||||
await WriteSeedFileAsync(component);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logger.LogError(ex, "Custom component seed file could not be synchronized for {Id}: {Message}", id, ex.Message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<string> WriteSeedFileAsync(CustomComponent component)
|
|
||||||
{
|
|
||||||
var safeFileName = GetSafeFileName(component.Name)
|
|
||||||
?? throw new InvalidOperationException($"Invalid component name for a seed file: '{component.Name}'.");
|
|
||||||
|
|
||||||
var outputPath = ResolveCustomSeedOutputPath();
|
|
||||||
Directory.CreateDirectory(outputPath);
|
|
||||||
|
|
||||||
var filePath = Path.Combine(outputPath, $"{safeFileName}.json");
|
|
||||||
|
|
||||||
var seedFile = new CustomComponentSeedFileDto
|
|
||||||
{
|
|
||||||
GeneratedAt = Clock.Now.ToUniversalTime(),
|
|
||||||
CustomComponents =
|
|
||||||
[
|
|
||||||
new CustomComponentSeedItemDto
|
|
||||||
{
|
|
||||||
Name = component.Name,
|
|
||||||
RoutePath = component.RoutePath,
|
|
||||||
Code = component.Code,
|
|
||||||
Props = component.Props,
|
|
||||||
Description = component.Description,
|
|
||||||
IsActive = component.IsActive,
|
|
||||||
Dependencies = ParseDependencies(component.Dependencies),
|
|
||||||
DataSources = ParseDataSources(component.DataSources)
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
await File.WriteAllTextAsync(filePath, JsonSerializer.Serialize(seedFile, SeedJsonWriteOptions));
|
|
||||||
Logger.LogInformation("Custom component seed file saved: {FilePath}", filePath);
|
|
||||||
|
|
||||||
return filePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DeleteSeedFile(string componentName)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var safeFileName = GetSafeFileName(componentName);
|
|
||||||
if (safeFileName == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var filePath = Path.Combine(ResolveCustomSeedOutputPath(), $"{safeFileName}.json");
|
|
||||||
if (File.Exists(filePath))
|
|
||||||
{
|
|
||||||
File.Delete(filePath);
|
|
||||||
Logger.LogInformation("Custom component seed file deleted: {FilePath}", filePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logger.LogError(ex, "Custom component seed file could not be deleted for {Name}: {Message}", componentName, ex.Message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -241,74 +147,4 @@ public class CustomComponentAppService : CrudAppService<
|
||||||
|
|
||||||
return CustomComponentDataSourceResolver.Resolve(props, references);
|
return CustomComponentDataSourceResolver.Resolve(props, references);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Entity'de JSON string olarak tutulan endpoint listesini cozer.</summary>
|
|
||||||
private static List<CustomComponentSeedDataSourceDto> ParseDataSources(string? dataSources)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(dataSources))
|
|
||||||
{
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return JsonSerializer.Deserialize<List<CustomComponentSeedDataSourceDto>>(dataSources, JsonReadOptions) ?? [];
|
|
||||||
}
|
|
||||||
catch (JsonException)
|
|
||||||
{
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Designer dokumanindaki tek bir data source kaydi.</summary>
|
|
||||||
private sealed class DesignerDataSourceDto
|
|
||||||
{
|
|
||||||
public string? Name { get; set; }
|
|
||||||
public string? Method { get; set; }
|
|
||||||
public string? Url { get; set; }
|
|
||||||
public string? ResponsePath { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Entity'de JSON string olarak tutulan bagimlilik listesini cozer.</summary>
|
|
||||||
private static List<string> ParseDependencies(string? dependencies)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(dependencies))
|
|
||||||
{
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return JsonSerializer.Deserialize<List<string>>(dependencies, JsonReadOptions) ?? [];
|
|
||||||
}
|
|
||||||
catch (JsonException)
|
|
||||||
{
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Custom component seed dosyalarinin yazilacagi klasor; kapsam aktif tenant'i izler
|
|
||||||
/// (host baglaminda <c>host/custom</c>, tenant baglaminda <c>tenants/{tenantId}/custom</c>).
|
|
||||||
/// </summary>
|
|
||||||
private string ResolveCustomSeedOutputPath()
|
|
||||||
{
|
|
||||||
return SeedPathResolver.GetScopePath(_configuration, CurrentTenant.Id, SeedPathResolver.CustomFolder);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Component adindan guvenli bir dosya adi uretir; path traversal denemelerini reddeder.</summary>
|
|
||||||
private static string? GetSafeFileName(string componentName)
|
|
||||||
{
|
|
||||||
var trimmed = componentName?.Trim();
|
|
||||||
if (string.IsNullOrWhiteSpace(trimmed) ||
|
|
||||||
trimmed.Contains('/') ||
|
|
||||||
trimmed.Contains('\\') ||
|
|
||||||
trimmed.Contains(".."))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var safeFileName = string.Concat(trimmed.Split(Path.GetInvalidFileNameChars()));
|
|
||||||
return string.IsNullOrWhiteSpace(safeFileName) ? null : safeFileName;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ public class ListFormWizardAppService(
|
||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
||||||
LanguageTextAppService languageTextAppService,
|
LanguageTextAppService languageTextAppService,
|
||||||
IDynamicDataManager dynamicDataManager,
|
IDynamicDataManager dynamicDataManager,
|
||||||
|
IListFormSeedDataApplier seedDataApplier,
|
||||||
ILogger<ListFormWizardAppService> logger
|
ILogger<ListFormWizardAppService> logger
|
||||||
) : PlatformAppService(), IListFormWizardAppService
|
) : PlatformAppService(), IListFormWizardAppService
|
||||||
{
|
{
|
||||||
|
|
@ -70,6 +71,7 @@ public class ListFormWizardAppService(
|
||||||
private readonly IConfiguration _configuration = configuration;
|
private readonly IConfiguration _configuration = configuration;
|
||||||
private readonly LanguageTextAppService _languageTextAppService = languageTextAppService;
|
private readonly LanguageTextAppService _languageTextAppService = languageTextAppService;
|
||||||
private readonly IDynamicDataManager _dynamicDataManager = dynamicDataManager;
|
private readonly IDynamicDataManager _dynamicDataManager = dynamicDataManager;
|
||||||
|
private readonly IListFormSeedDataApplier _seedDataApplier = seedDataApplier;
|
||||||
private readonly ILogger<ListFormWizardAppService> logger = logger;
|
private readonly ILogger<ListFormWizardAppService> logger = logger;
|
||||||
private readonly string cultureNameDefault = PlatformConsts.DefaultLanguage;
|
private readonly string cultureNameDefault = PlatformConsts.DefaultLanguage;
|
||||||
|
|
||||||
|
|
@ -534,6 +536,11 @@ public class ListFormWizardAppService(
|
||||||
})];
|
})];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Kapsamdaki wizard seed dosyalarini listeler. Sira ilk deploy damgasina (<c>SeededAt</c>)
|
||||||
|
/// gore tersinedir — en son uretilen ekran listenin basinda durur; dosya adina gore siralamak
|
||||||
|
/// yeni eklenen ekrani listenin ortasinda birakiyordu. Damgasi okunamayan dosyalar sona duser.
|
||||||
|
/// </summary>
|
||||||
public async Task<List<WizardFileInfoDto>> GetFiles()
|
public async Task<List<WizardFileInfoDto>> GetFiles()
|
||||||
{
|
{
|
||||||
var outputPath = ResolveWizardSeedOutputPath();
|
var outputPath = ResolveWizardSeedOutputPath();
|
||||||
|
|
@ -575,7 +582,10 @@ public class ListFormWizardAppService(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
// CreatedAt sortable ("s") bicimindedir; metin karsilastirmasi da kronolojiktir.
|
||||||
|
return [.. result
|
||||||
|
.OrderByDescending(x => x.CreatedAt, StringComparer.Ordinal)
|
||||||
|
.ThenBy(x => x.FileName, StringComparer.OrdinalIgnoreCase)];
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<WizardSeedFileDto> GetFile(string fileName)
|
public async Task<WizardSeedFileDto> GetFile(string fileName)
|
||||||
|
|
@ -804,8 +814,9 @@ public class ListFormWizardAppService(
|
||||||
/// Zip icindeki yollar seed kapsam klasoru (host / tenants/{tenantId}) koku baz alinarak yazilir;
|
/// Zip icindeki yollar seed kapsam klasoru (host / tenants/{tenantId}) koku baz alinarak yazilir;
|
||||||
/// dolayisiyla arsiv baska bir ortamda ayni klasor duzenine dogrudan acilabilir.
|
/// dolayisiyla arsiv baska bir ortamda ayni klasor duzenine dogrudan acilabilir.
|
||||||
/// <para>
|
/// <para>
|
||||||
/// Custom yolunda: <c>wizard/{dosya}.json</c>, <c>custom/{component}.json</c> (bagimliliklari ile birlikte)
|
/// Custom yolunda: <c>wizard/{dosya}.json</c>, secili component ve bagimliliklariyla suzulmus
|
||||||
/// ve bu component'lerin data source'larinin isaret ettigi <c>crud/{entity}.json</c> dosyalari.
|
/// <c>data/App.DeveloperKit.CustomComponents.json</c> ve bu component'lerin data source'larinin
|
||||||
|
/// isaret ettigi <c>crud/{entity}.json</c> dosyalari.
|
||||||
/// List yolunda: <c>wizard/{dosya}.json</c>, SelectCommand'a karsilik gelen
|
/// List yolunda: <c>wizard/{dosya}.json</c>, SelectCommand'a karsilik gelen
|
||||||
/// <c>{sql|postgres}/{object|execute}/{nesne}.sql</c> ve varsa <c>crud/{nesne}.json</c>.
|
/// <c>{sql|postgres}/{object|execute}/{nesne}.sql</c> ve varsa <c>crud/{nesne}.json</c>.
|
||||||
/// </para>
|
/// </para>
|
||||||
|
|
@ -828,8 +839,12 @@ public class ListFormWizardAppService(
|
||||||
[$"{SeedPathResolver.WizardFolder}/{Path.GetFileName(filePath)}"] = filePath
|
[$"{SeedPathResolver.WizardFolder}/{Path.GetFileName(filePath)}"] = filePath
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Paylasilan bir seed dosyasindan yalnizca ilgili satirlar suzuldugunde zip'e diskteki
|
||||||
|
// hali degil, uretilen icerik girer.
|
||||||
|
var generatedEntries = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
if (seed.Wizard?.ComponentKind == WizardComponentKindEnum.Custom)
|
if (seed.Wizard?.ComponentKind == WizardComponentKindEnum.Custom)
|
||||||
await CollectCustomComponentEntriesAsync(scopeRoot, seed.Wizard.CustomComponentName, entries);
|
await CollectCustomComponentEntriesAsync(scopeRoot, seed.Wizard.CustomComponentName, entries, generatedEntries);
|
||||||
else
|
else
|
||||||
CollectListComponentEntries(scopeRoot, seed.Wizard, entries);
|
CollectListComponentEntries(scopeRoot, seed.Wizard, entries);
|
||||||
|
|
||||||
|
|
@ -843,6 +858,13 @@ public class ListFormWizardAppService(
|
||||||
await using var target = zipEntry.Open();
|
await using var target = zipEntry.Open();
|
||||||
await source.CopyToAsync(target);
|
await source.CopyToAsync(target);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach (var entry in generatedEntries.OrderBy(x => x.Key, StringComparer.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var zipEntry = archive.CreateEntry(entry.Key, CompressionLevel.Optimal);
|
||||||
|
await using var target = zipEntry.Open();
|
||||||
|
await target.WriteAsync(ImportEncoding.GetBytes(entry.Value));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
memory.Position = 0;
|
memory.Position = 0;
|
||||||
|
|
@ -869,6 +891,12 @@ public class ListFormWizardAppService(
|
||||||
private const string ImportBackupFolder = "backup";
|
private const string ImportBackupFolder = "backup";
|
||||||
private const string ImportAppliedLogFile = "applied.log";
|
private const string ImportAppliedLogFile = "applied.log";
|
||||||
|
|
||||||
|
/// <summary>Custom component'lerin seed kaynagi olan ListForm ekrani.</summary>
|
||||||
|
private const string CustomComponentsListFormCode = SeedConsts.AppCodes.DeveloperKits.CustomComponents;
|
||||||
|
|
||||||
|
/// <summary>Custom component satirlarinin anahtar alani; bagimliliklar bu adla eslesir.</summary>
|
||||||
|
private const string CustomComponentNameField = "Name";
|
||||||
|
|
||||||
private const long ImportMaxEntryBytes = 5 * 1024 * 1024;
|
private const long ImportMaxEntryBytes = 5 * 1024 * 1024;
|
||||||
private const long ImportMaxArchiveBytes = 50 * 1024 * 1024;
|
private const long ImportMaxArchiveBytes = 50 * 1024 * 1024;
|
||||||
private const int ImportMaxEntryCount = 500;
|
private const int ImportMaxEntryCount = 500;
|
||||||
|
|
@ -878,7 +906,7 @@ public class ListFormWizardAppService(
|
||||||
[
|
[
|
||||||
SeedPathResolver.WizardFolder,
|
SeedPathResolver.WizardFolder,
|
||||||
SeedPathResolver.CrudFolder,
|
SeedPathResolver.CrudFolder,
|
||||||
SeedPathResolver.CustomFolder,
|
SeedPathResolver.DataFolder,
|
||||||
SeedPathResolver.SqlFolder,
|
SeedPathResolver.SqlFolder,
|
||||||
SeedPathResolver.PostgresFolder,
|
SeedPathResolver.PostgresFolder,
|
||||||
];
|
];
|
||||||
|
|
@ -1003,6 +1031,24 @@ public class ListFormWizardAppService(
|
||||||
var incomingText = NormalizeImportText(incoming);
|
var incomingText = NormalizeImportText(incoming);
|
||||||
var existingText = NormalizeImportText(existing);
|
var existingText = NormalizeImportText(existing);
|
||||||
|
|
||||||
|
// Veri seed dosyasinda karsilastirma dosya degil satir bazlidir.
|
||||||
|
var scope = IsSeedDataPath(relativePath) ? BuildSeedDataScope(incoming, existing) : null;
|
||||||
|
if (scope != null)
|
||||||
|
{
|
||||||
|
incomingText = scope.Incoming;
|
||||||
|
existingText = scope.Existing;
|
||||||
|
entry.IncomingSize = ImportEncoding.GetByteCount(scope.Incoming);
|
||||||
|
entry.ExistingSize = ImportEncoding.GetByteCount(scope.Existing);
|
||||||
|
|
||||||
|
// Gelen satirlarin hicbiri hedefte yoksa dosya var olsa da islem eklemedir.
|
||||||
|
if (!scope.HasExistingRows)
|
||||||
|
{
|
||||||
|
entry.Status = WizardImportEntryStatusEnum.New;
|
||||||
|
result.Entries.Add(entry);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (string.Equals(incomingText, existingText, StringComparison.Ordinal))
|
if (string.Equals(incomingText, existingText, StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
entry.Status = WizardImportEntryStatusEnum.Identical;
|
entry.Status = WizardImportEntryStatusEnum.Identical;
|
||||||
|
|
@ -1058,6 +1104,10 @@ public class ListFormWizardAppService(
|
||||||
: ImportEncoding.GetBytes(input.Content);
|
: ImportEncoding.GetBytes(input.Content);
|
||||||
|
|
||||||
var overwritten = File.Exists(targetPath);
|
var overwritten = File.Exists(targetPath);
|
||||||
|
|
||||||
|
if (overwritten && relativePath.StartsWith(SeedPathResolver.DataFolder + "/", StringComparison.OrdinalIgnoreCase))
|
||||||
|
content = await MergeSeedDataRowsAsync(targetPath, content);
|
||||||
|
|
||||||
if (overwritten)
|
if (overwritten)
|
||||||
{
|
{
|
||||||
var backupPath = Path.Combine(sessionPath, ImportBackupFolder, systemPath);
|
var backupPath = Path.Combine(sessionPath, ImportBackupFolder, systemPath);
|
||||||
|
|
@ -1087,12 +1137,106 @@ public class ListFormWizardAppService(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Import oturumunu kapatir. Kapanistan once zip'in getirdigi <c>data/*.json</c> dosyalari
|
||||||
|
/// veritabanina uygulanir: tabloda olmayan satirlar eklenir, var olanlar dosyadaki degerlerle
|
||||||
|
/// guncellenir ve soft delete edilmis satirlarin silinme izleri temizlenir — aksi halde kayit
|
||||||
|
/// tabloda durur ama ekranin varsayilan suzgeci onu gizlemeye devam ederdi. Hedefle zaten ayni
|
||||||
|
/// olan dosyalar da uygulanir: yazacak bir sey yoktur ama satirlar veritabaninda eksik ya da
|
||||||
|
/// silinmis olabilir. Migrate/seed akisindan farki budur — orada dosya bayat olabilir ve canli kayit
|
||||||
|
/// ezilmez; import ise kullanicinin bilincli getirdigi icerigi tasir. Diger klasorler
|
||||||
|
/// (wizard, crud, sql) dosya olarak kalir; onlar seeder ya da wizard deploy ile veritabanina gecer.
|
||||||
|
/// <para>
|
||||||
|
/// Uygulama basarisiz olsa bile oturum kapatilir; dosyalar hedefe yazilmis durumdadir ve
|
||||||
|
/// hata sonuca tasinir. Bu adim geri alinamaz, cunku dosya yedegi veritabani kaydini
|
||||||
|
/// kapsamaz — geri alma <c>RollbackImport</c> ile bu adimdan once yapilir.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
[Authorize(AppCodes.Listforms.Wizards.Import)]
|
[Authorize(AppCodes.Listforms.Wizards.Import)]
|
||||||
[HttpPost("api/app/list-form-wizard/complete-import")]
|
[HttpPost("api/app/list-form-wizard/complete-import")]
|
||||||
public Task CompleteImport(string importId)
|
public async Task<WizardImportCompleteResultDto> CompleteImport(string importId)
|
||||||
{
|
{
|
||||||
DeleteImportSession(GetExistingImportSessionPath(importId));
|
var sessionPath = GetExistingImportSessionPath(importId);
|
||||||
return Task.CompletedTask;
|
var scopeRoot = Path.GetFullPath(SeedPathResolver.GetScopePath(_configuration, CurrentTenant.Id));
|
||||||
|
var result = new WizardImportCompleteResultDto();
|
||||||
|
|
||||||
|
var dataFiles = await ReadAppliedDataFilesAsync(sessionPath, scopeRoot);
|
||||||
|
if (dataFiles.Count > 0)
|
||||||
|
{
|
||||||
|
foreach (var applied in await _seedDataApplier.ApplyFilesAsync(CurrentTenant.Id, dataFiles, overwriteExisting: true))
|
||||||
|
{
|
||||||
|
result.AppliedData.Add(new WizardImportSeedApplyResultDto
|
||||||
|
{
|
||||||
|
Path = applied.Path,
|
||||||
|
InsertedRowCount = applied.Inserted,
|
||||||
|
UpdatedRowCount = applied.Updated,
|
||||||
|
SkippedRowCount = applied.Skipped,
|
||||||
|
Error = applied.Error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DeleteImportSession(sessionPath);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Veritabanina uygulanacak <c>data/</c> girdilerini toplar.
|
||||||
|
/// <para>
|
||||||
|
/// Once oturumda gercekten yazilmis dosyalar (kayit dosyasindan) alinir. Ardindan yazilmamis
|
||||||
|
/// staging girdilerinden <b>hedefle ayni olanlar</b> eklenir: dosya ayni oldugu icin yazacak
|
||||||
|
/// bir sey yoktu, ama satirlar veritabaninda eksik ya da soft delete edilmis olabilir. Hedeften
|
||||||
|
/// farkli olup yazilmamis dosya kullanicinin "mevcudu koru" karari demektir; ona dokunulmaz.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
private async Task<List<string>> ReadAppliedDataFilesAsync(string sessionPath, string scopeRoot)
|
||||||
|
{
|
||||||
|
var prefix = SeedPathResolver.DataFolder + "/";
|
||||||
|
var paths = new List<string>();
|
||||||
|
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
var logPath = Path.Combine(sessionPath, ImportAppliedLogFile);
|
||||||
|
if (File.Exists(logPath))
|
||||||
|
{
|
||||||
|
foreach (var line in await File.ReadAllLinesAsync(logPath))
|
||||||
|
{
|
||||||
|
var parts = line.Split('|', 2);
|
||||||
|
if (parts.Length != 2 || !parts[1].StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (seen.Add(parts[1]))
|
||||||
|
paths.Add(parts[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var stagedDataPath = Path.Combine(sessionPath, ImportStagedFolder, SeedPathResolver.DataFolder);
|
||||||
|
if (!Directory.Exists(stagedDataPath))
|
||||||
|
return paths;
|
||||||
|
|
||||||
|
foreach (var stagedFile in Directory.GetFiles(stagedDataPath, "*.json"))
|
||||||
|
{
|
||||||
|
var relativePath = prefix + Path.GetFileName(stagedFile);
|
||||||
|
if (seen.Contains(relativePath))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var targetPath = ResolveScopeFilePath(scopeRoot, relativePath);
|
||||||
|
if (targetPath == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var staged = await File.ReadAllBytesAsync(stagedFile);
|
||||||
|
var existing = await File.ReadAllBytesAsync(targetPath);
|
||||||
|
|
||||||
|
// Karsilastirma analiz adimiyla ayni olmali: veri dosyalarinda satir bazlidir.
|
||||||
|
var scope = BuildSeedDataScope(staged, existing);
|
||||||
|
var isIdentical = scope != null
|
||||||
|
? string.Equals(scope.Incoming, scope.Existing, StringComparison.Ordinal)
|
||||||
|
: string.Equals(NormalizeImportText(staged), NormalizeImportText(existing), StringComparison.Ordinal);
|
||||||
|
|
||||||
|
if (isIdentical && seen.Add(relativePath))
|
||||||
|
paths.Add(relativePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return paths;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Authorize(AppCodes.Listforms.Wizards.Import)]
|
[Authorize(AppCodes.Listforms.Wizards.Import)]
|
||||||
|
|
@ -1143,6 +1287,156 @@ public class ListFormWizardAppService(
|
||||||
DeleteImportSession(sessionPath);
|
DeleteImportSession(sessionPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Kapsam-goreli yolun bir veri seed dosyasini gosterip gostermedigi.</summary>
|
||||||
|
private static bool IsSeedDataPath(string relativePath)
|
||||||
|
{
|
||||||
|
return relativePath.StartsWith(SeedPathResolver.DataFolder + "/", StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Veri seed dosyasinin karsilastirilacak kisimlari. <see cref="Existing"/> hedef dosyadan
|
||||||
|
/// yalnizca gelen satirlarin anahtarlariyla suzulur; <see cref="HasExistingRows"/> false ise
|
||||||
|
/// gelen satirlarin hicbiri hedefte yoktur.
|
||||||
|
/// </summary>
|
||||||
|
private sealed record SeedDataScope(string Incoming, string Existing, bool HasExistingRows);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Diff'te gosterilen suzulmus veri seed icerigi. Dosya duzeyindeki <c>GeneratedAt</c> ve
|
||||||
|
/// <c>Order</c> disarida birakilir; birlestirme bu alanlari hedeften korudugu icin
|
||||||
|
/// karsilastirmaya girmeleri sahte fark uretirdi.
|
||||||
|
/// </summary>
|
||||||
|
private sealed class ListFormSeedDataScopeDto
|
||||||
|
{
|
||||||
|
public string ListFormCode { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string KeyFieldName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public List<Dictionary<string, object>> Rows { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bir veri seed dosyasi kapsamdaki butun satirlari tasir, zip ise yalnizca aktarilan ekranin
|
||||||
|
/// satirlarini. Diff'te dosyanin tamami karsilastirilirsa kullanici ilgisiz kayitlarin farkini
|
||||||
|
/// gorur; bu yuzden iki taraf da gelen satirlarin anahtarlarina indirgenir. Dosya duzeyindeki
|
||||||
|
/// alanlar (<c>GeneratedAt</c>, <c>Order</c>) karsilastirmaya girmez — birlestirme onlari zaten
|
||||||
|
/// hedeften korur. Dosyalardan biri cozulemiyorsa null doner ve dosyanin tamami karsilastirilir.
|
||||||
|
/// </summary>
|
||||||
|
private static SeedDataScope? BuildSeedDataScope(byte[] incoming, byte[] existing)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var incomingFile = JsonSerializer.Deserialize<ListFormSeedDataFileDto>(incoming, SeedJsonReadOptions);
|
||||||
|
var existingFile = JsonSerializer.Deserialize<ListFormSeedDataFileDto>(existing, SeedJsonReadOptions);
|
||||||
|
|
||||||
|
var keyFieldName = incomingFile?.KeyFieldName ?? existingFile?.KeyFieldName;
|
||||||
|
if (incomingFile == null || existingFile == null || string.IsNullOrWhiteSpace(keyFieldName))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var existingByKey = new Dictionary<string, Dictionary<string, object>>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var row in existingFile.Rows)
|
||||||
|
{
|
||||||
|
var key = GetRowText(row, keyFieldName);
|
||||||
|
if (!string.IsNullOrWhiteSpace(key))
|
||||||
|
existingByKey[key] = row;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hedef satirlar gelen sirayla dizilir; sira farki diff'te sahte fark uretmesin.
|
||||||
|
var matched = new List<Dictionary<string, object>>();
|
||||||
|
foreach (var row in incomingFile.Rows)
|
||||||
|
{
|
||||||
|
var key = GetRowText(row, keyFieldName);
|
||||||
|
if (!string.IsNullOrWhiteSpace(key) && existingByKey.TryGetValue(key, out var match))
|
||||||
|
matched.Add(match);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new SeedDataScope(
|
||||||
|
SerializeSeedDataScope(incomingFile, keyFieldName, incomingFile.Rows),
|
||||||
|
SerializeSeedDataScope(existingFile, keyFieldName, matched),
|
||||||
|
matched.Count > 0);
|
||||||
|
}
|
||||||
|
catch (JsonException)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Suzulmus satirlari, birlestirmenin okudugu alanlarla (ekran kodu, anahtar alan, satirlar)
|
||||||
|
/// yeniden yazar. Kullanici diff ekraninda bu icerigi duzenleyip gonderdiginde de
|
||||||
|
/// <see cref="MergeSeedDataRowsAsync"/> ayni sozlesmeyi bulur.
|
||||||
|
/// </summary>
|
||||||
|
private static string SerializeSeedDataScope(
|
||||||
|
ListFormSeedDataFileDto source,
|
||||||
|
string keyFieldName,
|
||||||
|
List<Dictionary<string, object>> rows)
|
||||||
|
{
|
||||||
|
return JsonSerializer.Serialize(
|
||||||
|
new ListFormSeedDataScopeDto
|
||||||
|
{
|
||||||
|
ListFormCode = source.ListFormCode,
|
||||||
|
KeyFieldName = keyFieldName,
|
||||||
|
Rows = rows,
|
||||||
|
},
|
||||||
|
ListFormSeedDataFileDto.JsonOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bir veri seed dosyasi kapsamdaki butun satirlari tasir, zip ise yalnizca aktarilan ekranin
|
||||||
|
/// satirlarini getirir; bu yuzden hedef dosyanin ustune yazilmaz, satirlar anahtar alan
|
||||||
|
/// uzerinden birlestirilir: gelen satir hedefte varsa guncellenir, yoksa sona eklenir,
|
||||||
|
/// hedefteki diger satirlar korunur. Iki dosyadan biri cozulemiyorsa ya da anahtar alan
|
||||||
|
/// bilinmiyorsa gelen icerik oldugu gibi yazilir.
|
||||||
|
/// </summary>
|
||||||
|
private async Task<byte[]> MergeSeedDataRowsAsync(string targetPath, byte[] incoming)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var incomingFile = JsonSerializer.Deserialize<ListFormSeedDataFileDto>(incoming, SeedJsonReadOptions);
|
||||||
|
var existingFile = JsonSerializer.Deserialize<ListFormSeedDataFileDto>(
|
||||||
|
await File.ReadAllBytesAsync(targetPath), SeedJsonReadOptions);
|
||||||
|
|
||||||
|
var keyFieldName = incomingFile?.KeyFieldName ?? existingFile?.KeyFieldName;
|
||||||
|
if (incomingFile == null || existingFile == null || string.IsNullOrWhiteSpace(keyFieldName))
|
||||||
|
return incoming;
|
||||||
|
|
||||||
|
var merged = new List<Dictionary<string, object>>(existingFile.Rows);
|
||||||
|
var indexByKey = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
for (var i = 0; i < merged.Count; i++)
|
||||||
|
{
|
||||||
|
var key = GetRowText(merged[i], keyFieldName);
|
||||||
|
if (!string.IsNullOrWhiteSpace(key))
|
||||||
|
indexByKey[key] = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var row in incomingFile.Rows)
|
||||||
|
{
|
||||||
|
var key = GetRowText(row, keyFieldName);
|
||||||
|
if (string.IsNullOrWhiteSpace(key))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (indexByKey.TryGetValue(key, out var index))
|
||||||
|
{
|
||||||
|
merged[index] = row;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
indexByKey[key] = merged.Count;
|
||||||
|
merged.Add(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
existingFile.Rows = merged;
|
||||||
|
existingFile.GeneratedAt = Clock.Now.ToUniversalTime();
|
||||||
|
|
||||||
|
return ImportEncoding.GetBytes(JsonSerializer.Serialize(existingFile, ListFormSeedDataFileDto.JsonOptions));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogWarning(ex, "Seed data file could not be merged, incoming content is written as is: {TargetPath}", targetPath);
|
||||||
|
return incoming;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Zip girdisini kapsam klasorune goreli yola cevirir. Zip, kapsam klasorunun icinden
|
/// Zip girdisini kapsam klasorune goreli yola cevirir. Zip, kapsam klasorunun icinden
|
||||||
/// (<c>wizard/x.json</c>) ya da depo kokunden (<c>configs/seeds/host/wizard/x.json</c>)
|
/// (<c>wizard/x.json</c>) ya da depo kokunden (<c>configs/seeds/host/wizard/x.json</c>)
|
||||||
|
|
@ -1255,63 +1549,120 @@ public class ListFormWizardAppService(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Custom component seed dosyasini, bagimli oldugu component'leri ve bunlarin data source'larinin
|
/// Custom component'lerin seed kaynagi <c>data/App.DeveloperKit.CustomComponents.json</c>
|
||||||
/// isaret ettigi crud seed dosyalarini toplar. Bagimlilik zinciri dongu icerse bile
|
/// dosyasidir. Dosya kapsamdaki butun component'leri tasidigi icin zip'e oldugu gibi degil,
|
||||||
/// ziyaret edilen adlar takip edildigi icin sonsuz dongu olusmaz.
|
/// secili component ile bagimlilik zincirine suzulmus bir kopyasi konur; boylece arsiv ilgisiz
|
||||||
|
/// component'leri disari tasimaz. Ayni zincirdeki component'lerin data source'larinin isaret
|
||||||
|
/// ettigi crud seed dosyalari da toplanir. Bagimlilik zinciri dongu icerse bile ziyaret edilen
|
||||||
|
/// adlar takip edildigi icin sonsuz dongu olusmaz.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task CollectCustomComponentEntriesAsync(string scopeRoot, string? componentName, Dictionary<string, string> entries)
|
private async Task CollectCustomComponentEntriesAsync(
|
||||||
|
string scopeRoot,
|
||||||
|
string? componentName,
|
||||||
|
Dictionary<string, string> entries,
|
||||||
|
Dictionary<string, string> generatedEntries)
|
||||||
{
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(componentName))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var relativePath = SeedPathResolver.GetDataFilePath(CustomComponentsListFormCode);
|
||||||
|
var sourcePath = ResolveScopeFilePath(scopeRoot, relativePath);
|
||||||
|
if (sourcePath == null)
|
||||||
|
{
|
||||||
|
logger.LogWarning("Custom component seed data file not found for export: {RelativePath}", relativePath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ListFormSeedDataFileDto? seedFile;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
seedFile = JsonSerializer.Deserialize<ListFormSeedDataFileDto>(
|
||||||
|
await File.ReadAllTextAsync(sourcePath), SeedJsonReadOptions);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogWarning(ex, "Custom component seed data file could not be parsed for export: {RelativePath}", relativePath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seedFile == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var rowsByName = new Dictionary<string, Dictionary<string, object>>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var row in seedFile.Rows)
|
||||||
|
{
|
||||||
|
var name = GetRowText(row, CustomComponentNameField);
|
||||||
|
if (!string.IsNullOrWhiteSpace(name))
|
||||||
|
rowsByName[name] = row;
|
||||||
|
}
|
||||||
|
|
||||||
var pending = new Queue<string>();
|
var pending = new Queue<string>();
|
||||||
var visited = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
var visited = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
var exported = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
if (!string.IsNullOrWhiteSpace(componentName))
|
pending.Enqueue(componentName.Trim());
|
||||||
pending.Enqueue(componentName.Trim());
|
|
||||||
|
|
||||||
while (pending.Count > 0)
|
while (pending.Count > 0)
|
||||||
{
|
{
|
||||||
var name = pending.Dequeue();
|
var name = pending.Dequeue();
|
||||||
if (!visited.Add(name))
|
if (!visited.Add(name) || !rowsByName.TryGetValue(name, out var row))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var relativePath = $"{SeedPathResolver.CustomFolder}/{name}.json";
|
exported.Add(name);
|
||||||
if (!TryAddSeedEntry(scopeRoot, relativePath, entries))
|
|
||||||
|
foreach (var dependency in ParseSeedRowList<string>(GetRowText(row, "Dependencies")))
|
||||||
{
|
{
|
||||||
logger.LogWarning("Custom component seed file not found for export: {RelativePath}", relativePath);
|
if (!string.IsNullOrWhiteSpace(dependency))
|
||||||
continue;
|
pending.Enqueue(dependency.Trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
CustomComponentSeedFileDto? customSeed;
|
foreach (var dataSource in ParseSeedRowList<CustomComponentSeedDataSourceDto>(GetRowText(row, "DataSources")))
|
||||||
try
|
|
||||||
{
|
{
|
||||||
var customJson = await File.ReadAllTextAsync(entries[relativePath]);
|
// SeedFile yalnizca CRUD Endpoint Manager'in urettigi endpointlerde doludur;
|
||||||
customSeed = JsonSerializer.Deserialize<CustomComponentSeedFileDto>(customJson, SeedJsonReadOptions);
|
// elle yazilmis bir URL'in seed karsiligi yoktur.
|
||||||
}
|
if (string.IsNullOrWhiteSpace(dataSource.SeedFile))
|
||||||
catch (Exception ex)
|
continue;
|
||||||
{
|
|
||||||
logger.LogWarning(ex, "Custom component seed file could not be parsed for export: {RelativePath}", relativePath);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var component in customSeed?.CustomComponents ?? [])
|
if (!TryAddSeedEntry(scopeRoot, dataSource.SeedFile, entries))
|
||||||
{
|
logger.LogWarning("CRUD seed file not found for export: {RelativePath}", dataSource.SeedFile);
|
||||||
foreach (var dependency in component.Dependencies)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrWhiteSpace(dependency))
|
|
||||||
pending.Enqueue(dependency.Trim());
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var dataSource in component.DataSources)
|
|
||||||
{
|
|
||||||
// SeedFile yalnizca CRUD Endpoint Manager'in urettigi endpointlerde doludur;
|
|
||||||
// elle yazilmis bir URL'in seed karsiligi yoktur.
|
|
||||||
if (string.IsNullOrWhiteSpace(dataSource.SeedFile))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (!TryAddSeedEntry(scopeRoot, dataSource.SeedFile, entries))
|
|
||||||
logger.LogWarning("CRUD seed file not found for export: {RelativePath}", dataSource.SeedFile);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (exported.Count == 0)
|
||||||
|
{
|
||||||
|
logger.LogWarning("Custom component not found in the seed data file for export: {ComponentName}", componentName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dosyadaki satir sirasi korunur; hedefte satirlar bu sirayla birlestirilir.
|
||||||
|
seedFile.Rows = [.. seedFile.Rows.Where(x => exported.Contains(GetRowText(x, CustomComponentNameField) ?? string.Empty))];
|
||||||
|
generatedEntries[relativePath] = JsonSerializer.Serialize(seedFile, ListFormSeedDataFileDto.JsonOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Seed satirindaki bir alani metin olarak okur; JsonElement ve duz deger destekler.</summary>
|
||||||
|
private static string? GetRowText(Dictionary<string, object> row, string fieldName)
|
||||||
|
{
|
||||||
|
if (!row.TryGetValue(fieldName, out var value) || value is null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return value is JsonElement element
|
||||||
|
? (element.ValueKind == JsonValueKind.String ? element.GetString() : element.ToString())
|
||||||
|
: value.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Seed satirinda JSON dizisi olarak tutulan alanlari cozer; bozuk deger bos liste doner.</summary>
|
||||||
|
private static List<T> ParseSeedRowList<T>(string? json)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(json))
|
||||||
|
return [];
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return JsonSerializer.Deserialize<List<T>>(json, SeedJsonReadOptions) ?? [];
|
||||||
|
}
|
||||||
|
catch (JsonException)
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -1369,20 +1720,30 @@ public class ListFormWizardAppService(
|
||||||
/// sessizce reddedilir. Dosya yoksa false doner.
|
/// sessizce reddedilir. Dosya yoksa false doner.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static bool TryAddSeedEntry(string scopeRoot, string relativePath, Dictionary<string, string> entries)
|
private static bool TryAddSeedEntry(string scopeRoot, string relativePath, Dictionary<string, string> entries)
|
||||||
|
{
|
||||||
|
var fullPath = ResolveScopeFilePath(scopeRoot, relativePath);
|
||||||
|
if (fullPath == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
entries[relativePath.Replace('\\', '/').Trim('/')] = fullPath;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Kapsam klasorune goreli yolu var olan bir dosyanin tam yoluna cevirir; kapsam disina cikan
|
||||||
|
/// ya da bulunmayan bir yol icin null doner.
|
||||||
|
/// </summary>
|
||||||
|
private static string? ResolveScopeFilePath(string scopeRoot, string relativePath)
|
||||||
{
|
{
|
||||||
var normalized = relativePath.Replace('\\', '/').Trim('/');
|
var normalized = relativePath.Replace('\\', '/').Trim('/');
|
||||||
if (normalized.Length == 0 || normalized.Contains("..", StringComparison.Ordinal))
|
if (normalized.Length == 0 || normalized.Contains("..", StringComparison.Ordinal))
|
||||||
return false;
|
return null;
|
||||||
|
|
||||||
var fullPath = Path.GetFullPath(Path.Combine(scopeRoot, normalized.Replace('/', Path.DirectorySeparatorChar)));
|
var fullPath = Path.GetFullPath(Path.Combine(scopeRoot, normalized.Replace('/', Path.DirectorySeparatorChar)));
|
||||||
if (!fullPath.StartsWith(scopeRoot + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase))
|
if (!fullPath.StartsWith(scopeRoot + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase))
|
||||||
return false;
|
return null;
|
||||||
|
|
||||||
if (!File.Exists(fullPath))
|
return File.Exists(fullPath) ? fullPath : null;
|
||||||
return false;
|
|
||||||
|
|
||||||
entries[normalized] = fullPath;
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<HashSet<string>> GetTableColumnNamesAsync(string dataSourceCode, SelectCommandTypeEnum commandType, string selectCommand)
|
private async Task<HashSet<string>> GetTableColumnNamesAsync(string dataSourceCode, SelectCommandTypeEnum commandType, string selectCommand)
|
||||||
|
|
|
||||||
|
|
@ -1,206 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Sozsoft.Platform.Entities;
|
|
||||||
using Volo.Abp.Data;
|
|
||||||
using Volo.Abp.DependencyInjection;
|
|
||||||
using Volo.Abp.Domain.Repositories;
|
|
||||||
|
|
||||||
namespace Sozsoft.Platform.Data.Seeds;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Component Manager ile olusturulan custom component'leri
|
|
||||||
/// <c>{host|tenants/{tenantId}}/custom/*.json</c> dosyalarindan okuyarak veritabanina uygular
|
|
||||||
/// (kapsam, seed edilen veritabanini izler).
|
|
||||||
/// <para>
|
|
||||||
/// Dosyalar component kaydedildikce/silindikce otomatik yazildigi icin kaynak kabul edilir:
|
|
||||||
/// ayni adli component varsa dosyadaki icerikle guncellenir, yoksa olusturulur.
|
|
||||||
/// Dosyada olmayan component'ler silinmez.
|
|
||||||
/// </para>
|
|
||||||
/// </summary>
|
|
||||||
public class CustomComponentDataSeeder : IDataSeedContributor, ITransientDependency
|
|
||||||
{
|
|
||||||
private static readonly JsonSerializerOptions JsonReadOptions = new()
|
|
||||||
{
|
|
||||||
PropertyNameCaseInsensitive = true
|
|
||||||
};
|
|
||||||
|
|
||||||
private static readonly JsonSerializerOptions JsonWriteOptions = new()
|
|
||||||
{
|
|
||||||
WriteIndented = true
|
|
||||||
};
|
|
||||||
|
|
||||||
private readonly IRepository<CustomComponent, Guid> _repository;
|
|
||||||
private readonly IConfiguration _configuration;
|
|
||||||
private readonly ILogger<CustomComponentDataSeeder> _logger;
|
|
||||||
|
|
||||||
public CustomComponentDataSeeder(
|
|
||||||
IRepository<CustomComponent, Guid> repository,
|
|
||||||
IConfiguration configuration,
|
|
||||||
ILogger<CustomComponentDataSeeder> logger)
|
|
||||||
{
|
|
||||||
_repository = repository;
|
|
||||||
_configuration = configuration;
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task SeedAsync(DataSeedContext context)
|
|
||||||
{
|
|
||||||
var scopeFolderName = SeedPathResolver.GetScopeFolderName(context.TenantId);
|
|
||||||
var customDataPath = SeedPathResolver.GetScopePath(_configuration, context.TenantId, SeedPathResolver.CustomFolder);
|
|
||||||
|
|
||||||
if (!Directory.Exists(customDataPath))
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Seeds/{Scope}/custom directory not found, skipping CustomComponentDataSeeder.", scopeFolderName);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var jsonFiles = Directory.GetFiles(customDataPath, "*.json").OrderBy(Path.GetFileName).ToArray();
|
|
||||||
if (jsonFiles.Length == 0)
|
|
||||||
{
|
|
||||||
_logger.LogInformation("No JSON files found in Seeds/{Scope}/custom directory, skipping CustomComponentDataSeeder.", scopeFolderName);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Eski seed dosyalarinda DataSources blogu yoktur; katalog, bloksuz dosyalarda
|
|
||||||
// endpoint listesinin Props'tan turetilebilmesi icin bastan okunur.
|
|
||||||
var crudEndpoints = ReadCrudEndpointCatalog(context.TenantId, scopeFolderName);
|
|
||||||
|
|
||||||
_logger.LogInformation("CustomComponentDataSeeder started. {Count} file(s) to be processed.", jsonFiles.Length);
|
|
||||||
|
|
||||||
foreach (var filePath in jsonFiles)
|
|
||||||
{
|
|
||||||
var fileName = Path.GetFileName(filePath);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var json = await File.ReadAllTextAsync(filePath);
|
|
||||||
var seedFile = JsonSerializer.Deserialize<CustomComponentSeedFileDto>(json, JsonReadOptions);
|
|
||||||
|
|
||||||
if (seedFile?.CustomComponents == null || seedFile.CustomComponents.Count == 0)
|
|
||||||
{
|
|
||||||
_logger.LogWarning("[{File}] No custom component found, skipped.", fileName);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var item in seedFile.CustomComponents)
|
|
||||||
{
|
|
||||||
await ApplyAsync(fileName, item, crudEndpoints);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
// Tek bir dosyanin hatasi diger component'lerin seed edilmesini engellemez.
|
|
||||||
_logger.LogError(ex, "Failed to process custom component seed file: {FileName}", fileName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogInformation("CustomComponentDataSeeder completed. {Count} file(s) processed.", jsonFiles.Length);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Kapsamdaki <c>crud/*.json</c> dosyalarini okuyarak endpoint katalogunu olusturur.
|
|
||||||
/// Veritabani yerine dosyalardan okunur; boylece seed sirasi CrudEndpointDataSeeder'a bagli kalmaz.
|
|
||||||
/// </summary>
|
|
||||||
private List<CustomComponentDataSourceResolver.CrudEndpointReference> ReadCrudEndpointCatalog(
|
|
||||||
Guid? tenantId,
|
|
||||||
string scopeFolderName)
|
|
||||||
{
|
|
||||||
var references = new List<CustomComponentDataSourceResolver.CrudEndpointReference>();
|
|
||||||
var crudDataPath = SeedPathResolver.GetScopePath(_configuration, tenantId, SeedPathResolver.CrudFolder);
|
|
||||||
|
|
||||||
if (!Directory.Exists(crudDataPath))
|
|
||||||
{
|
|
||||||
return references;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var filePath in Directory.GetFiles(crudDataPath, "*.json"))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var seedFile = JsonSerializer.Deserialize<CrudEndpointSeedFileDto>(
|
|
||||||
File.ReadAllText(filePath),
|
|
||||||
JsonReadOptions);
|
|
||||||
|
|
||||||
if (seedFile?.Endpoints == null)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
references.AddRange(seedFile.Endpoints.Select(endpoint =>
|
|
||||||
new CustomComponentDataSourceResolver.CrudEndpointReference
|
|
||||||
{
|
|
||||||
EntityName = seedFile.EntityName,
|
|
||||||
Method = endpoint.Method,
|
|
||||||
Path = endpoint.Path,
|
|
||||||
OperationType = endpoint.OperationType
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
// Katalog yardimci bilgidir; okunamayan bir dosya component seed'ini durdurmaz.
|
|
||||||
_logger.LogWarning(ex, "[{Scope}] CRUD seed file could not be read for the data source catalog: {File}",
|
|
||||||
scopeFolderName, Path.GetFileName(filePath));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return references;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ApplyAsync(
|
|
||||||
string fileName,
|
|
||||||
CustomComponentSeedItemDto item,
|
|
||||||
List<CustomComponentDataSourceResolver.CrudEndpointReference> crudEndpoints)
|
|
||||||
{
|
|
||||||
var name = item.Name?.Trim();
|
|
||||||
if (string.IsNullOrWhiteSpace(name))
|
|
||||||
{
|
|
||||||
_logger.LogWarning("[{File}] Component name is empty, skipped.", fileName);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var dependencies = JsonSerializer.Serialize(item.Dependencies ?? new List<string>());
|
|
||||||
// DataSources blogu olmayan (bu sutundan once yazilmis) dosyalarda liste Props'taki
|
|
||||||
// designer dokumanindan turetilir; boylece sutun her durumda dolar.
|
|
||||||
var dataSourceItems = item.DataSources is { Count: > 0 }
|
|
||||||
? item.DataSources
|
|
||||||
: CustomComponentDataSourceResolver.Resolve(item.Props, crudEndpoints);
|
|
||||||
|
|
||||||
// Bos liste sutunu doldurmaya degmez; component'in hic endpoint'i yoksa null kalir.
|
|
||||||
var dataSources = dataSourceItems.Count > 0
|
|
||||||
? JsonSerializer.Serialize(dataSourceItems, JsonWriteOptions)
|
|
||||||
: null;
|
|
||||||
var current = await _repository.FirstOrDefaultAsync(x => x.Name == name);
|
|
||||||
|
|
||||||
if (current == null)
|
|
||||||
{
|
|
||||||
await _repository.InsertAsync(new CustomComponent(
|
|
||||||
name,
|
|
||||||
item.RoutePath,
|
|
||||||
item.Code,
|
|
||||||
item.Props,
|
|
||||||
item.Description,
|
|
||||||
item.IsActive,
|
|
||||||
dependencies,
|
|
||||||
dataSources), autoSave: true);
|
|
||||||
|
|
||||||
_logger.LogInformation("[{File}] '{Name}' created.", fileName, name);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
current.RoutePath = item.RoutePath;
|
|
||||||
current.Code = item.Code;
|
|
||||||
current.Props = item.Props;
|
|
||||||
current.Description = item.Description;
|
|
||||||
current.IsActive = item.IsActive;
|
|
||||||
current.Dependencies = dependencies;
|
|
||||||
current.DataSources = dataSources;
|
|
||||||
|
|
||||||
await _repository.UpdateAsync(current, autoSave: true);
|
|
||||||
_logger.LogInformation("[{File}] '{Name}' updated.", fileName, name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -732,12 +732,6 @@
|
||||||
"en": "Crud Endpoints",
|
"en": "Crud Endpoints",
|
||||||
"tr": "Crud Uç Noktaları"
|
"tr": "Crud Uç Noktaları"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"resourceName": "Platform",
|
|
||||||
"key": "App.DeveloperKit.Components",
|
|
||||||
"en": "Custom Components",
|
|
||||||
"tr": "Özel Bileşenler"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
"key": "App.DeveloperKit.DynamicServices",
|
"key": "App.DeveloperKit.DynamicServices",
|
||||||
|
|
@ -9026,15 +9020,21 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
"key": "App.DeveloperKitComponent.ComponentDescription",
|
"key": "App.Listform.ListformField.DataSources",
|
||||||
"en": "Manage your custom React components",
|
"en": "Data Sources",
|
||||||
"tr": "Özel React bileşenlerinizi yönetin"
|
"tr": "Veri Kaynakları"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
"key": "App.DeveloperKitComponent.New",
|
"key": "App.DeveloperKit.CustomComponents",
|
||||||
"en": "New Component",
|
"en": "Custom Components",
|
||||||
"tr": "Yeni Bileşen"
|
"tr": "Özel Bileşenler"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"resourceName": "Platform",
|
||||||
|
"key": "App.DeveloperKit.CustomComponents.Description",
|
||||||
|
"en": "Custom components built with the Visual Designer",
|
||||||
|
"tr": "Visual Designer ile üretilen özel bileşenler"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
|
|
@ -9072,24 +9072,12 @@
|
||||||
"en": "Total number of inactive custom React components",
|
"en": "Total number of inactive custom React components",
|
||||||
"tr": "Pasif özel React bileşenlerinin toplam sayısı"
|
"tr": "Pasif özel React bileşenlerinin toplam sayısı"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"resourceName": "Platform",
|
|
||||||
"key": "App.DeveloperKitComponent.SearchPlaceholder",
|
|
||||||
"en": "Search components...",
|
|
||||||
"tr": "Bileşenlerde ara..."
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
"key": "App.ComponentFilter.All",
|
"key": "App.ComponentFilter.All",
|
||||||
"en": "All Components",
|
"en": "All Components",
|
||||||
"tr": "Tüm Bileşenler"
|
"tr": "Tüm Bileşenler"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"resourceName": "Platform",
|
|
||||||
"key": "App.DeveloperKitComponent.NoDependencies",
|
|
||||||
"en": "No dependencies",
|
|
||||||
"tr": "Bağımlılık yok"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
"key": "App.DeveloperKitComponent.SaveToSeed",
|
"key": "App.DeveloperKitComponent.SaveToSeed",
|
||||||
|
|
@ -9132,12 +9120,6 @@
|
||||||
"en": "Delete Component",
|
"en": "Delete Component",
|
||||||
"tr": "Bileşeni Sil"
|
"tr": "Bileşeni Sil"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"resourceName": "Platform",
|
|
||||||
"key": "App.DeveloperKitComponent.ConfirmDelete",
|
|
||||||
"en": "Are you sure you want to delete the component?",
|
|
||||||
"tr": "Bileşeni silmek istediğinizden emin misiniz?"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
"key": "App.EmptyFiltered.FilteredTitle",
|
"key": "App.EmptyFiltered.FilteredTitle",
|
||||||
|
|
@ -16455,8 +16437,8 @@
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
"key": "App.ListForm.WizardFileExport",
|
"key": "App.ListForm.WizardFileExport",
|
||||||
"en": "Export as zip (wizard, custom/crud and sql seed files)",
|
"en": "Export as zip (wizard, data/crud and sql seed files)",
|
||||||
"tr": "Zip olarak dışa aktar (wizard, custom/crud ve sql seed dosyaları)"
|
"tr": "Zip olarak dışa aktar (wizard, data/crud ve sql seed dosyaları)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
|
|
@ -27231,8 +27213,26 @@
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
"key": "App.ListForm.WizardImportRunMigrateHint",
|
"key": "App.ListForm.WizardImportRunMigrateHint",
|
||||||
"en": "Run DB Migrate to load the imported seed files into the database.",
|
"en": "Data rows were written to the database. Run DB Migrate for the remaining seed files (wizard, crud, sql).",
|
||||||
"tr": "İçe aktarılan seed dosyalarının veritabanına işlenmesi için DB Migrate çalıştırın."
|
"tr": "Veri satırları veritabanına yazıldı. Kalan seed dosyaları (wizard, crud, sql) için DB Migrate çalıştırın."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"resourceName": "Platform",
|
||||||
|
"key": "App.ListForm.WizardImportDataApplied",
|
||||||
|
"en": "Rows written to the database",
|
||||||
|
"tr": "Veritabanına yazılan satırlar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"resourceName": "Platform",
|
||||||
|
"key": "App.ListForm.WizardImportDataRowSummary",
|
||||||
|
"en": "{inserted} inserted, {updated} updated, {skipped} unchanged",
|
||||||
|
"tr": "{inserted} eklendi, {updated} güncellendi, {skipped} değişmedi"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"resourceName": "Platform",
|
||||||
|
"key": "App.ListForm.WizardImportDataApplyFailed",
|
||||||
|
"en": "Files were written but some rows could not be applied to the database.",
|
||||||
|
"tr": "Dosyalar yazıldı ancak bazı satırlar veritabanına uygulanamadı."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
|
|
|
||||||
|
|
@ -6897,9 +6897,9 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
|
||||||
SelectCommandType = SelectCommandTypeEnum.Table,
|
SelectCommandType = SelectCommandTypeEnum.Table,
|
||||||
SelectCommand = TableNameResolver.GetFullTableName(nameof(TableNameEnum.CustomEndpoint)),
|
SelectCommand = TableNameResolver.GetFullTableName(nameof(TableNameEnum.CustomEndpoint)),
|
||||||
SeedFilePath = SeedPathResolver.GetDataFilePath(listFormName),
|
SeedFilePath = SeedPathResolver.GetDataFilePath(listFormName),
|
||||||
SeedSyncInsert = false,
|
SeedSyncInsert = true,
|
||||||
SeedSyncUpdate = false,
|
SeedSyncUpdate = true,
|
||||||
SeedSyncDelete = false,
|
SeedSyncDelete = true,
|
||||||
KeyFieldName = "Id",
|
KeyFieldName = "Id",
|
||||||
KeyFieldDbSourceType = DbType.Guid,
|
KeyFieldDbSourceType = DbType.Guid,
|
||||||
DefaultFilter = DefaultFilterJson,
|
DefaultFilter = DefaultFilterJson,
|
||||||
|
|
@ -7179,9 +7179,9 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
|
||||||
SelectCommandType = SelectCommandTypeEnum.Table,
|
SelectCommandType = SelectCommandTypeEnum.Table,
|
||||||
SelectCommand = dynamicServiceTable,
|
SelectCommand = dynamicServiceTable,
|
||||||
SeedFilePath = SeedPathResolver.GetDataFilePath(listFormName),
|
SeedFilePath = SeedPathResolver.GetDataFilePath(listFormName),
|
||||||
SeedSyncInsert = false,
|
SeedSyncInsert = true,
|
||||||
SeedSyncUpdate = false,
|
SeedSyncUpdate = true,
|
||||||
SeedSyncDelete = false,
|
SeedSyncDelete = true,
|
||||||
KeyFieldName = "Id",
|
KeyFieldName = "Id",
|
||||||
KeyFieldDbSourceType = DbType.Guid,
|
KeyFieldDbSourceType = DbType.Guid,
|
||||||
DefaultFilter = DefaultFilterJson,
|
DefaultFilter = DefaultFilterJson,
|
||||||
|
|
@ -7527,6 +7527,322 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region Custom Components
|
||||||
|
listFormName = AppCodes.DeveloperKits.CustomComponents;
|
||||||
|
if (!await _listFormRepository.AnyAsync(a => a.ListFormCode == listFormName))
|
||||||
|
{
|
||||||
|
// Ekran bilesenin ust bilgilerini yonetir; kod ve tasarim Visual Designer sayfasindadir.
|
||||||
|
var customComponentTable = TableNameResolver.GetFullTableName(nameof(TableNameEnum.CustomComponent));
|
||||||
|
|
||||||
|
var listForm = await _listFormRepository.InsertAsync(
|
||||||
|
new ListForm()
|
||||||
|
{
|
||||||
|
ListFormType = ListFormTypeEnum.List,
|
||||||
|
PageSize = 10,
|
||||||
|
ExportJson = DefaultExportJson,
|
||||||
|
IsSubForm = false,
|
||||||
|
ShowNote = false,
|
||||||
|
LayoutJson = JsonSerializer.Serialize(new LayoutDto
|
||||||
|
{
|
||||||
|
Grid = true,
|
||||||
|
Card = true,
|
||||||
|
Pivot = false,
|
||||||
|
Chart = false,
|
||||||
|
Tree = false,
|
||||||
|
Gantt = false,
|
||||||
|
Scheduler = false,
|
||||||
|
Todo = false,
|
||||||
|
DefaultLayout = "grid",
|
||||||
|
}),
|
||||||
|
CultureName = LanguageCodes.En,
|
||||||
|
ListFormCode = listFormName,
|
||||||
|
Name = listFormName,
|
||||||
|
Title = listFormName,
|
||||||
|
DataSourceCode = SeedConsts.DataSources.DefaultCode,
|
||||||
|
IsTenant = false,
|
||||||
|
IsBranch = false,
|
||||||
|
IsOrganizationUnit = false,
|
||||||
|
Description = $"{listFormName}.Description",
|
||||||
|
SelectCommandType = SelectCommandTypeEnum.Table,
|
||||||
|
SelectCommand = customComponentTable,
|
||||||
|
SeedFilePath = SeedPathResolver.GetDataFilePath(listFormName),
|
||||||
|
SeedSyncInsert = true,
|
||||||
|
SeedSyncUpdate = true,
|
||||||
|
SeedSyncDelete = true,
|
||||||
|
KeyFieldName = "Id",
|
||||||
|
KeyFieldDbSourceType = DbType.Guid,
|
||||||
|
DefaultFilter = DefaultFilterJson,
|
||||||
|
SortMode = GridOptions.SortModeSingle,
|
||||||
|
FilterRowJson = DefaultFilterRowJson(),
|
||||||
|
HeaderFilterJson = DefaultHeaderFilterJson(),
|
||||||
|
SearchPanelJson = DefaultSearchPanelJson(),
|
||||||
|
GroupPanelJson = DefaultGroupPanelJson(),
|
||||||
|
SelectionJson = DefaultSelectionSingleJson,
|
||||||
|
ColumnOptionJson = DefaultColumnOptionJson(),
|
||||||
|
PermissionJson = DefaultPermissionJson(listFormName),
|
||||||
|
PagerOptionJson = DefaultPagerOptionJson,
|
||||||
|
// Ekleme/duzenleme grid'in kendi popup formunda; kod tarafi Design butonundan acilir.
|
||||||
|
EditingOptionJson = DefaultEditingOptionJson(listFormName, 700, 520, true, true, true, true, false),
|
||||||
|
EditingFormJson = JsonSerializer.Serialize(new List<EditingFormDto>()
|
||||||
|
{
|
||||||
|
new() { Order = 1, ColCount = 2, ColSpan = 1, ItemType = "group" },
|
||||||
|
}),
|
||||||
|
InsertFieldsDefaultValueJson = JsonSerializer.Serialize(new FieldsDefaultValue[] {
|
||||||
|
new() { FieldName = "CreationTime", FieldDbType = DbType.DateTimeOffset, Value = "@NOW", CustomValueType = FieldCustomValueTypeEnum.CustomKey },
|
||||||
|
new() { FieldName = "CreatorId", FieldDbType = DbType.Guid, Value = "@USERID", CustomValueType = FieldCustomValueTypeEnum.CustomKey },
|
||||||
|
new() { FieldName = "Id", FieldDbType = DbType.Guid, Value = "@NEWID", CustomValueType = FieldCustomValueTypeEnum.CustomKey },
|
||||||
|
// Kod ve tasarim dokumani tasarimcida uretilir; kolon NOT NULL oldugu icin bos baslar.
|
||||||
|
new() { FieldName = "Code", FieldDbType = DbType.String, Value = "", CustomValueType = FieldCustomValueTypeEnum.Value },
|
||||||
|
}),
|
||||||
|
FormFieldsDefaultValueJson = JsonSerializer.Serialize(new FieldsDefaultValue[] {
|
||||||
|
new() { FieldName = "IsActive", FieldDbType = DbType.Boolean, Value = "true", CustomValueType = FieldCustomValueTypeEnum.Value },
|
||||||
|
}),
|
||||||
|
DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.CustomComponent)),
|
||||||
|
DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(),
|
||||||
|
CommandColumnJson = JsonSerializer.Serialize(new CommandColumnDto[] {
|
||||||
|
new() {
|
||||||
|
ButtonPosition = UiCommandButtonPositionTypeEnum.CommandColumn,
|
||||||
|
Hint = "App.Platform.Design",
|
||||||
|
Text = "App.Platform.Design",
|
||||||
|
AuthName = listFormName + ".Update",
|
||||||
|
Url = "/admin/developerkit/components/design/@Id",
|
||||||
|
UrlTarget = "_blank",
|
||||||
|
IsVisible = true,
|
||||||
|
},
|
||||||
|
new() {
|
||||||
|
ButtonPosition = UiCommandButtonPositionTypeEnum.CommandColumn,
|
||||||
|
Hint = "App.Platform.OpenUrl",
|
||||||
|
Text = "App.Platform.OpenUrl",
|
||||||
|
AuthName = listFormName,
|
||||||
|
Url = "@RoutePath",
|
||||||
|
UrlTarget = "_blank",
|
||||||
|
// Pasif ya da rotasi olmayan bilesenin onizlemesi acilmaz.
|
||||||
|
VisibleExpression = "(e) => Boolean(e.row.data.IsActive) && Boolean(e.row.data.RoutePath)",
|
||||||
|
IsVisible = true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
WidgetsJson = JsonSerializer.Serialize(new List<WidgetEditDto>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
ColGap = 3,
|
||||||
|
ColSpan = 4,
|
||||||
|
SqlQuery = $@"
|
||||||
|
SELECT
|
||||||
|
'App.DeveloperKitComponent.Total' AS ""Title"",
|
||||||
|
COUNT(*) AS ""Value"",
|
||||||
|
'purple' AS ""Color"",
|
||||||
|
'App.DeveloperKitComponent.TotalDescription' AS ""SubTitle"",
|
||||||
|
'FaPuzzlePiece' AS ""Icon""
|
||||||
|
FROM ""{customComponentTable}""
|
||||||
|
WHERE ""IsDeleted"" = 'false'
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
'App.DeveloperKitComponent.Active' AS ""Title"",
|
||||||
|
COUNT(*) AS ""Value"",
|
||||||
|
'green' AS ""Color"",
|
||||||
|
'App.DeveloperKitComponent.ActiveDescription' AS ""SubTitle"",
|
||||||
|
'FaCheckCircle' AS ""Icon""
|
||||||
|
FROM ""{customComponentTable}""
|
||||||
|
WHERE ""IsDeleted"" = 'false' AND ""IsActive"" = 'true'
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
'App.DeveloperKitComponent.Inactive' AS ""Title"",
|
||||||
|
COUNT(*) AS ""Value"",
|
||||||
|
'gray' AS ""Color"",
|
||||||
|
'App.DeveloperKitComponent.InactiveDescription' AS ""SubTitle"",
|
||||||
|
'FaTimesCircle' AS ""Icon""
|
||||||
|
FROM ""{customComponentTable}""
|
||||||
|
WHERE ""IsDeleted"" = 'false' AND ""IsActive"" = 'false'
|
||||||
|
",
|
||||||
|
Title = "Title",
|
||||||
|
Value = "Value",
|
||||||
|
ValueClassName = "text-3xl",
|
||||||
|
Color = "Color",
|
||||||
|
Icon = "Icon",
|
||||||
|
SubTitle = "SubTitle",
|
||||||
|
ClassName = "mb-3",
|
||||||
|
IsActive = true
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
#region Custom Components Fields
|
||||||
|
await _listFormFieldRepository.InsertManyAsync([
|
||||||
|
new() {
|
||||||
|
ListFormCode = listForm.ListFormCode,
|
||||||
|
CultureName = LanguageCodes.En,
|
||||||
|
SourceDbType = DbType.Guid,
|
||||||
|
FieldName = "Id",
|
||||||
|
CaptionName = "App.Listform.ListformField.Id",
|
||||||
|
Width = 0,
|
||||||
|
ListOrderNo = 1,
|
||||||
|
Visible = false,
|
||||||
|
IsActive = true,
|
||||||
|
|
||||||
|
ColumnCustomizationJson = DefaultColumnCustomizationJson,
|
||||||
|
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
|
||||||
|
PivotSettingsJson = DefaultPivotSettingsJson
|
||||||
|
},
|
||||||
|
new() {
|
||||||
|
ListFormCode = listForm.ListFormCode,
|
||||||
|
CultureName = LanguageCodes.En,
|
||||||
|
SourceDbType = DbType.String,
|
||||||
|
FieldName = "Name",
|
||||||
|
EditGroupOrderNo = 1,
|
||||||
|
EditOrderNo = 1,
|
||||||
|
EditorType2 = EditorTypes.dxTextBox,
|
||||||
|
ColSpan = 1,
|
||||||
|
CaptionName = "App.Listform.ListformField.Name",
|
||||||
|
Width = 0,
|
||||||
|
ListOrderNo = 2,
|
||||||
|
Visible = true,
|
||||||
|
IsActive = true,
|
||||||
|
|
||||||
|
AllowSearch = true,
|
||||||
|
ValidationRuleJson = DefaultValidationRuleRequiredJson,
|
||||||
|
ColumnCustomizationJson = DefaultColumnCustomizationJson,
|
||||||
|
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
|
||||||
|
PivotSettingsJson = DefaultPivotSettingsJson
|
||||||
|
},
|
||||||
|
new() {
|
||||||
|
ListFormCode = listForm.ListFormCode,
|
||||||
|
CultureName = LanguageCodes.En,
|
||||||
|
SourceDbType = DbType.String,
|
||||||
|
FieldName = "RoutePath",
|
||||||
|
EditGroupOrderNo = 1,
|
||||||
|
EditOrderNo = 2,
|
||||||
|
EditorType2 = EditorTypes.dxTextBox,
|
||||||
|
ColSpan = 1,
|
||||||
|
CaptionName = "App.Platform.RoutePath",
|
||||||
|
Width = 0,
|
||||||
|
ListOrderNo = 3,
|
||||||
|
Visible = true,
|
||||||
|
IsActive = true,
|
||||||
|
|
||||||
|
AllowSearch = true,
|
||||||
|
// Rota "/" ile baslamalidir; sunucu tarafinda da ayni kural dogrulanir.
|
||||||
|
ValidationRuleJson = JsonSerializer.Serialize(new ValidationRuleDto[] {
|
||||||
|
new() { Type = Enum.GetName(UiColumnValidationRuleTypeEnum.required) },
|
||||||
|
new() { Type = Enum.GetName(UiColumnValidationRuleTypeEnum.pattern), Pattern = "^/.*", Message = "Route path must start with /" },
|
||||||
|
}),
|
||||||
|
ColumnCustomizationJson = DefaultColumnCustomizationJson,
|
||||||
|
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
|
||||||
|
PivotSettingsJson = DefaultPivotSettingsJson
|
||||||
|
},
|
||||||
|
new() {
|
||||||
|
ListFormCode = listForm.ListFormCode,
|
||||||
|
CultureName = LanguageCodes.En,
|
||||||
|
SourceDbType = DbType.String,
|
||||||
|
FieldName = "Description",
|
||||||
|
EditGroupOrderNo = 1,
|
||||||
|
EditOrderNo = 3,
|
||||||
|
EditorType2 = EditorTypes.dxTextArea,
|
||||||
|
ColSpan = 2,
|
||||||
|
EditorOptions = EditorOptions.Multiline(60),
|
||||||
|
CaptionName = "App.Listform.ListformField.Description",
|
||||||
|
Width = 0,
|
||||||
|
ListOrderNo = 4,
|
||||||
|
Visible = true,
|
||||||
|
IsActive = true,
|
||||||
|
|
||||||
|
AllowSearch = true,
|
||||||
|
ColumnCustomizationJson = DefaultColumnCustomizationJson,
|
||||||
|
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
|
||||||
|
PivotSettingsJson = DefaultPivotSettingsJson
|
||||||
|
},
|
||||||
|
new() {
|
||||||
|
ListFormCode = listForm.ListFormCode,
|
||||||
|
CultureName = LanguageCodes.En,
|
||||||
|
SourceDbType = DbType.Boolean,
|
||||||
|
FieldName = "IsActive",
|
||||||
|
EditGroupOrderNo = 1,
|
||||||
|
EditOrderNo = 4,
|
||||||
|
EditorType2 = EditorTypes.dxCheckBox,
|
||||||
|
ColSpan = 1,
|
||||||
|
CaptionName = "App.Listform.ListformField.IsActive",
|
||||||
|
Width = 0,
|
||||||
|
ListOrderNo = 5,
|
||||||
|
Visible = true,
|
||||||
|
IsActive = true,
|
||||||
|
|
||||||
|
AllowSearch = true,
|
||||||
|
ColumnCustomizationJson = DefaultColumnCustomizationJson,
|
||||||
|
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
|
||||||
|
PivotSettingsJson = DefaultPivotSettingsJson
|
||||||
|
},
|
||||||
|
new() {
|
||||||
|
ListFormCode = listForm.ListFormCode,
|
||||||
|
CultureName = LanguageCodes.En,
|
||||||
|
SourceDbType = DbType.String,
|
||||||
|
FieldName = "Dependencies",
|
||||||
|
EditorType2 = EditorTypes.dxTextBox,
|
||||||
|
CaptionName = "App.Platform.Dependencies",
|
||||||
|
Width = 0,
|
||||||
|
ListOrderNo = 6,
|
||||||
|
Visible = true,
|
||||||
|
IsActive = true,
|
||||||
|
|
||||||
|
AllowSearch = true,
|
||||||
|
ColumnCustomizationJson = DefaultColumnCustomizationJson,
|
||||||
|
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
|
||||||
|
PivotSettingsJson = DefaultPivotSettingsJson
|
||||||
|
},
|
||||||
|
new() {
|
||||||
|
ListFormCode = listForm.ListFormCode,
|
||||||
|
CultureName = LanguageCodes.En,
|
||||||
|
SourceDbType = DbType.String,
|
||||||
|
FieldName = "Code",
|
||||||
|
CaptionName = "App.Listform.ListformField.Code",
|
||||||
|
Width = 0,
|
||||||
|
ListOrderNo = 7,
|
||||||
|
Visible = false,
|
||||||
|
IsActive = true,
|
||||||
|
|
||||||
|
ColumnCustomizationJson = DefaultColumnCustomizationJson,
|
||||||
|
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
|
||||||
|
PivotSettingsJson = DefaultPivotSettingsJson
|
||||||
|
},
|
||||||
|
new() {
|
||||||
|
ListFormCode = listForm.ListFormCode,
|
||||||
|
CultureName = LanguageCodes.En,
|
||||||
|
SourceDbType = DbType.String,
|
||||||
|
FieldName = "Props",
|
||||||
|
CaptionName = "App.Listform.ListformField.Props",
|
||||||
|
Width = 0,
|
||||||
|
ListOrderNo = 8,
|
||||||
|
Visible = false,
|
||||||
|
IsActive = true,
|
||||||
|
|
||||||
|
ColumnCustomizationJson = DefaultColumnCustomizationJson,
|
||||||
|
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
|
||||||
|
PivotSettingsJson = DefaultPivotSettingsJson
|
||||||
|
},
|
||||||
|
new() {
|
||||||
|
ListFormCode = listForm.ListFormCode,
|
||||||
|
CultureName = LanguageCodes.En,
|
||||||
|
SourceDbType = DbType.String,
|
||||||
|
FieldName = "DataSources",
|
||||||
|
CaptionName = "App.Listform.ListformField.DataSources",
|
||||||
|
Width = 0,
|
||||||
|
ListOrderNo = 9,
|
||||||
|
Visible = false,
|
||||||
|
IsActive = true,
|
||||||
|
|
||||||
|
ColumnCustomizationJson = DefaultColumnCustomizationJson,
|
||||||
|
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
|
||||||
|
PivotSettingsJson = DefaultPivotSettingsJson
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
#region Products
|
#region Products
|
||||||
listFormName = AppCodes.Orders.Products;
|
listFormName = AppCodes.Orders.Products;
|
||||||
if (!await _listFormRepository.AnyAsync(a => a.ListFormCode == listFormName))
|
if (!await _listFormRepository.AnyAsync(a => a.ListFormCode == listFormName))
|
||||||
|
|
|
||||||
|
|
@ -329,20 +329,12 @@
|
||||||
"authority": ["App.SqlQueryManager"]
|
"authority": ["App.SqlQueryManager"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "admin.developerkit.components",
|
"key": "admin.developerkit.components.design",
|
||||||
"path": "/admin/developerkit/components",
|
"path": "/admin/developerkit/components/design/:id",
|
||||||
"componentType": "normal",
|
|
||||||
"componentPath": "@/views/developerKit/ComponentManagerPage",
|
|
||||||
"routeType": "protected",
|
|
||||||
"authority": ["App.DeveloperKit.Components"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "admin.developerkit.components.edit",
|
|
||||||
"path": "/admin/developerkit/components/edit/:id",
|
|
||||||
"componentType": "normal",
|
"componentType": "normal",
|
||||||
"componentPath": "@/views/developerKit/ComponentCodeLayout",
|
"componentPath": "@/views/developerKit/ComponentCodeLayout",
|
||||||
"routeType": "protected",
|
"routeType": "protected",
|
||||||
"authority": ["App.DeveloperKit.Components"]
|
"authority": ["App.DeveloperKit.CustomComponents"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "admin.intranet.documents",
|
"key": "admin.intranet.documents",
|
||||||
|
|
@ -997,12 +989,12 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ParentCode": "App.DeveloperKit",
|
"ParentCode": "App.DeveloperKit",
|
||||||
"Code": "App.DeveloperKit.Components",
|
"Code": "App.DeveloperKit.CustomComponents",
|
||||||
"DisplayName": "App.DeveloperKit.Components",
|
"DisplayName": "App.DeveloperKit.CustomComponents",
|
||||||
"Order": 5,
|
"Order": 5,
|
||||||
"Url": "/admin/developerkit/components",
|
"Url": "/admin/list/App.DeveloperKit.CustomComponents",
|
||||||
"Icon": "FcBiohazard",
|
"Icon": "FcBiohazard",
|
||||||
"RequiredPermissionName": "App.DeveloperKit.Components",
|
"RequiredPermissionName": "App.DeveloperKit.CustomComponents",
|
||||||
"IsDisabled": false,
|
"IsDisabled": false,
|
||||||
"ModuleId": "Sas"
|
"ModuleId": "Sas"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -2374,7 +2374,7 @@
|
||||||
"GroupName": "App.Saas",
|
"GroupName": "App.Saas",
|
||||||
"Name": "App.DeveloperKit.CustomEndpoints.Put",
|
"Name": "App.DeveloperKit.CustomEndpoints.Put",
|
||||||
"ParentName": "App.DeveloperKit.CustomEndpoints",
|
"ParentName": "App.DeveloperKit.CustomEndpoints",
|
||||||
"DisplayName": "Put",
|
"DisplayName": "App.StaticLookup.Put",
|
||||||
"IsEnabled": true,
|
"IsEnabled": true,
|
||||||
"MultiTenancySide": 2,
|
"MultiTenancySide": 2,
|
||||||
"Modules": "Sas|Adm"
|
"Modules": "Sas|Adm"
|
||||||
|
|
@ -2471,17 +2471,17 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"GroupName": "App.Saas",
|
"GroupName": "App.Saas",
|
||||||
"Name": "App.DeveloperKit.Components",
|
"Name": "App.DeveloperKit.CustomComponents",
|
||||||
"ParentName": "App.DeveloperKit",
|
"ParentName": "App.DeveloperKit",
|
||||||
"DisplayName": "App.DeveloperKit.Components",
|
"DisplayName": "App.DeveloperKit.CustomComponents",
|
||||||
"IsEnabled": true,
|
"IsEnabled": true,
|
||||||
"MultiTenancySide": 2,
|
"MultiTenancySide": 2,
|
||||||
"Modules": "Sas|Adm"
|
"Modules": "Sas|Adm"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"GroupName": "App.Saas",
|
"GroupName": "App.Saas",
|
||||||
"Name": "App.DeveloperKit.Components.Create",
|
"Name": "App.DeveloperKit.CustomComponents.Create",
|
||||||
"ParentName": "App.DeveloperKit.Components",
|
"ParentName": "App.DeveloperKit.CustomComponents",
|
||||||
"DisplayName": "App.Platform.Create",
|
"DisplayName": "App.Platform.Create",
|
||||||
"IsEnabled": true,
|
"IsEnabled": true,
|
||||||
"MultiTenancySide": 2,
|
"MultiTenancySide": 2,
|
||||||
|
|
@ -2489,8 +2489,8 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"GroupName": "App.Saas",
|
"GroupName": "App.Saas",
|
||||||
"Name": "App.DeveloperKit.Components.Update",
|
"Name": "App.DeveloperKit.CustomComponents.Update",
|
||||||
"ParentName": "App.DeveloperKit.Components",
|
"ParentName": "App.DeveloperKit.CustomComponents",
|
||||||
"DisplayName": "App.Platform.Update",
|
"DisplayName": "App.Platform.Update",
|
||||||
"IsEnabled": true,
|
"IsEnabled": true,
|
||||||
"MultiTenancySide": 2,
|
"MultiTenancySide": 2,
|
||||||
|
|
@ -2498,13 +2498,31 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"GroupName": "App.Saas",
|
"GroupName": "App.Saas",
|
||||||
"Name": "App.DeveloperKit.Components.Delete",
|
"Name": "App.DeveloperKit.CustomComponents.Delete",
|
||||||
"ParentName": "App.DeveloperKit.Components",
|
"ParentName": "App.DeveloperKit.CustomComponents",
|
||||||
"DisplayName": "App.Platform.Delete",
|
"DisplayName": "App.Platform.Delete",
|
||||||
"IsEnabled": true,
|
"IsEnabled": true,
|
||||||
"MultiTenancySide": 2,
|
"MultiTenancySide": 2,
|
||||||
"Modules": "Sas|Adm"
|
"Modules": "Sas|Adm"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"GroupName": "App.Saas",
|
||||||
|
"Name": "App.DeveloperKit.CustomComponents.Export",
|
||||||
|
"ParentName": "App.DeveloperKit.CustomComponents",
|
||||||
|
"DisplayName": "Export",
|
||||||
|
"IsEnabled": true,
|
||||||
|
"MultiTenancySide": 2,
|
||||||
|
"Modules": "Sas|Adm"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"GroupName": "App.Saas",
|
||||||
|
"Name": "App.DeveloperKit.CustomComponents.Import",
|
||||||
|
"ParentName": "App.DeveloperKit.CustomComponents",
|
||||||
|
"DisplayName": "Import",
|
||||||
|
"IsEnabled": true,
|
||||||
|
"MultiTenancySide": 2,
|
||||||
|
"Modules": "Sas|Adm"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"GroupName": "App.Saas",
|
"GroupName": "App.Saas",
|
||||||
"Name": "App.DeveloperKit.IntranetWidgets",
|
"Name": "App.DeveloperKit.IntranetWidgets",
|
||||||
|
|
|
||||||
|
|
@ -494,7 +494,7 @@ public static class PlatformConsts
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class Components
|
public static class Components
|
||||||
{
|
{
|
||||||
public const string Component = Default + ".Components";
|
public const string Component = Default + ".CustomComponents";
|
||||||
|
|
||||||
public const string Create = Component + ".Create";
|
public const string Create = Component + ".Create";
|
||||||
public const string Update = Component + ".Update";
|
public const string Update = Component + ".Update";
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,8 @@ namespace Sozsoft.Platform.Data.Seeds;
|
||||||
/// Component'in designer dokumanindaki (Props) Data sekmesi endpoint'lerini cozup
|
/// Component'in designer dokumanindaki (Props) Data sekmesi endpoint'lerini cozup
|
||||||
/// CRUD endpoint katalogu ile eslestiren ortak mantik.
|
/// CRUD endpoint katalogu ile eslestiren ortak mantik.
|
||||||
/// <para>
|
/// <para>
|
||||||
/// Ayni cozum hem kaydetme sirasinda (CustomComponentAppService, katalog veritabanindan gelir)
|
/// Cozum kaydetme sirasinda (CustomComponentAppService) katalogu veritabanindan okur;
|
||||||
/// hem de seed sirasinda (CustomComponentDataSeeder, katalog <c>crud/*.json</c> dosyalarindan gelir)
|
/// boylece <c>DataSources</c> sutunu tasarimci dokumanindan tutarli sekilde uretilir.
|
||||||
/// kullanilir; boylece <c>DataSources</c> sutunu iki yolda da ayni sekilde uretilir.
|
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class CustomComponentDataSourceResolver
|
public static class CustomComponentDataSourceResolver
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
namespace Sozsoft.Platform.Data.Seeds;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Component'in kullandigi tek bir API. <see cref="EntityName"/> dolu ise endpoint
|
||||||
|
/// CRUD Endpoint Manager tarafindan uretilmistir ve tanimi <see cref="SeedFile"/>
|
||||||
|
/// (kapsam klasorune gore <c>crud/{EntityName}.json</c>) dosyasindadir.
|
||||||
|
/// </summary>
|
||||||
|
public class CustomComponentSeedDataSourceDto
|
||||||
|
{
|
||||||
|
/// <summary>Designer'daki gorunur ad.</summary>
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string Method { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string Url { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>Yanit icindeki liste yolu; bos ise yanitin kendisi kullanilir.</summary>
|
||||||
|
public string? ResponsePath { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Endpoint bir CRUD endpoint'ine denk geliyorsa entity adi, aksi halde null.</summary>
|
||||||
|
public string? EntityName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>CRUD endpoint'inin islem turu (GetAll, GetById, Create, Update, Delete).</summary>
|
||||||
|
public string? OperationType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Endpoint tanimini tasiyan seed dosyasinin kapsam klasorune gore yolu.</summary>
|
||||||
|
public string? SeedFile { get; set; }
|
||||||
|
}
|
||||||
|
|
@ -1,71 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace Sozsoft.Platform.Data.Seeds;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Bir custom component'in seed dosyasi.
|
|
||||||
/// Component Manager uzerinden kaydedildiginde/silindiginde
|
|
||||||
/// <c>{host|tenants/{tenantId}}/custom/{Name}.json</c> olarak yazilir; veritabani silinip
|
|
||||||
/// yeniden olusturuldugunda <c>CustomComponentDataSeeder</c> ayni dosyalari okuyarak
|
|
||||||
/// component'leri geri yukler.
|
|
||||||
/// <para>
|
|
||||||
/// Dosya duzeni bilincli olarak TenantData.json'daki <c>CustomComponents</c> bloguyla aynidir;
|
|
||||||
/// boylece eski seed icerigi dogrudan tasinabilir. Liste birden fazla component tasiyabilse de
|
|
||||||
/// senkronizasyon her component icin tek dosya yazar.
|
|
||||||
/// </para>
|
|
||||||
/// </summary>
|
|
||||||
public class CustomComponentSeedFileDto
|
|
||||||
{
|
|
||||||
/// <summary>Dosyanin en son yazildigi an (UTC) — yalnizca bilgi amaclidir.</summary>
|
|
||||||
public DateTime GeneratedAt { get; set; }
|
|
||||||
|
|
||||||
public List<CustomComponentSeedItemDto> CustomComponents { get; set; } = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Component'in seed edilebilir tam tanimi; dosya adi <see cref="Name"/> ile ayni olmasi beklenir.</summary>
|
|
||||||
public class CustomComponentSeedItemDto
|
|
||||||
{
|
|
||||||
public string Name { get; set; } = string.Empty;
|
|
||||||
public string RoutePath { get; set; } = string.Empty;
|
|
||||||
public string Code { get; set; } = string.Empty;
|
|
||||||
public string? Props { get; set; }
|
|
||||||
public string? Description { get; set; }
|
|
||||||
public bool IsActive { get; set; } = true;
|
|
||||||
|
|
||||||
/// <summary>Bagimli olunan component adlari; veritabaninda JSON string olarak tutulur.</summary>
|
|
||||||
public List<string> Dependencies { get; set; } = [];
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Component'in Data sekmesinde kullandigi API'ler; veritabaninda JSON string olarak tutulur.
|
|
||||||
/// Kaydetme sirasinda designer dokumanindan otomatik uretilir.
|
|
||||||
/// </summary>
|
|
||||||
public List<CustomComponentSeedDataSourceDto> DataSources { get; set; } = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Component'in kullandigi tek bir API. <see cref="EntityName"/> dolu ise endpoint
|
|
||||||
/// CRUD Endpoint Manager tarafindan uretilmistir ve tanimi <see cref="SeedFile"/>
|
|
||||||
/// (kapsam klasorune gore <c>crud/{EntityName}.json</c>) dosyasindadir.
|
|
||||||
/// </summary>
|
|
||||||
public class CustomComponentSeedDataSourceDto
|
|
||||||
{
|
|
||||||
/// <summary>Designer'daki gorunur ad.</summary>
|
|
||||||
public string Name { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
public string Method { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
public string Url { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>Yanit icindeki liste yolu; bos ise yanitin kendisi kullanilir.</summary>
|
|
||||||
public string? ResponsePath { get; set; }
|
|
||||||
|
|
||||||
/// <summary>Endpoint bir CRUD endpoint'ine denk geliyorsa entity adi, aksi halde null.</summary>
|
|
||||||
public string? EntityName { get; set; }
|
|
||||||
|
|
||||||
/// <summary>CRUD endpoint'inin islem turu (GetAll, GetById, Create, Update, Delete).</summary>
|
|
||||||
public string? OperationType { get; set; }
|
|
||||||
|
|
||||||
/// <summary>Endpoint tanimini tasiyan seed dosyasinin kapsam klasorune gore yolu.</summary>
|
|
||||||
public string? SeedFile { get; set; }
|
|
||||||
}
|
|
||||||
|
|
@ -28,6 +28,40 @@ public interface IListFormSeedDataApplier
|
||||||
{
|
{
|
||||||
/// <summary>Verilen kapsamdaki tum veri seed dosyalarini uygular.</summary>
|
/// <summary>Verilen kapsamdaki tum veri seed dosyalarini uygular.</summary>
|
||||||
Task ApplyAsync(Guid? tenantId);
|
Task ApplyAsync(Guid? tenantId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Kapsam klasorune goreli belirli veri seed dosyalarini uygular; import gibi dosya bazli
|
||||||
|
/// akislar butun klasoru taramak yerine yalnizca yazilan dosyalari uygular. Dosyalar
|
||||||
|
/// <c>ApplyAsync</c> ile ayni kurala gore siralanir.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="overwriteExisting">
|
||||||
|
/// true ise anahtari veritabaninda bulunan satir dosyadaki degerlerle guncellenir. Migrate/seed
|
||||||
|
/// akisi bunu kullanmaz — orada dosya eski olabilir ve canli kayit ezilmemelidir; import ise
|
||||||
|
/// kullanicinin bilincli olarak getirdigi icerik oldugu icin gunceller.
|
||||||
|
/// </param>
|
||||||
|
Task<IReadOnlyList<ListFormSeedDataApplyResult>> ApplyFilesAsync(
|
||||||
|
Guid? tenantId,
|
||||||
|
IReadOnlyCollection<string> relativePaths,
|
||||||
|
bool overwriteExisting = false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tek bir veri seed dosyasinin uygulama sonucu. <see cref="Error"/> dolu ise dosya islenemedi;
|
||||||
|
/// diger dosyalarin uygulanmasi bundan etkilenmez.
|
||||||
|
/// </summary>
|
||||||
|
public record ListFormSeedDataApplyResult(string Path, int Inserted, int Updated, int Skipped, string? Error = null);
|
||||||
|
|
||||||
|
/// <summary>Bir satirin veritabaninda ne olduguna dair sonuc.</summary>
|
||||||
|
public enum SeedRowOutcome
|
||||||
|
{
|
||||||
|
/// <summary>Kayit yoktu, eklendi.</summary>
|
||||||
|
Inserted,
|
||||||
|
|
||||||
|
/// <summary>Kayit vardi, dosyadaki degerlerle guncellendi.</summary>
|
||||||
|
Updated,
|
||||||
|
|
||||||
|
/// <summary>Kayit vardi, dokunulmadi.</summary>
|
||||||
|
Skipped,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -60,6 +94,50 @@ public class ListFormSeedDataApplier(
|
||||||
/// <summary>Cozulen kullanici anahtarlari; ayni kullanici her satirda yeniden sorgulanmaz.</summary>
|
/// <summary>Cozulen kullanici anahtarlari; ayni kullanici her satirda yeniden sorgulanmaz.</summary>
|
||||||
private readonly Dictionary<string, Guid?> userIdCache = [];
|
private readonly Dictionary<string, Guid?> userIdCache = [];
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<ListFormSeedDataApplyResult>> ApplyFilesAsync(
|
||||||
|
Guid? tenantId,
|
||||||
|
IReadOnlyCollection<string> relativePaths,
|
||||||
|
bool overwriteExisting = false)
|
||||||
|
{
|
||||||
|
if (relativePaths == null || relativePaths.Count == 0)
|
||||||
|
return [];
|
||||||
|
|
||||||
|
var scopeRoot = Path.GetFullPath(SeedPathResolver.GetScopePath(configuration, tenantId));
|
||||||
|
var results = new List<ListFormSeedDataApplyResult>();
|
||||||
|
var filePaths = new List<string>();
|
||||||
|
|
||||||
|
foreach (var relativePath in relativePaths)
|
||||||
|
{
|
||||||
|
var normalized = relativePath.Replace('\\', '/').Trim('/');
|
||||||
|
var fullPath = normalized.Contains("..", StringComparison.Ordinal)
|
||||||
|
? null
|
||||||
|
: Path.GetFullPath(Path.Combine(scopeRoot, normalized.Replace('/', Path.DirectorySeparatorChar)));
|
||||||
|
|
||||||
|
if (fullPath == null
|
||||||
|
|| !fullPath.StartsWith(scopeRoot + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| !File.Exists(fullPath))
|
||||||
|
{
|
||||||
|
results.Add(new ListFormSeedDataApplyResult(normalized, 0, 0, 0, "Seed data file not found."));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
filePaths.Add(fullPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
using (currentTenant.Change(tenantId))
|
||||||
|
{
|
||||||
|
foreach (var filePath in OrderFiles([.. filePaths]))
|
||||||
|
{
|
||||||
|
// Her dosya kendi unit of work'unde islenir; ApplyAsync ile ayni kural.
|
||||||
|
using var uow = unitOfWorkManager.Begin(requiresNew: true, isTransactional: false);
|
||||||
|
results.Add(await ApplyFileAsync(filePath, overwriteExisting));
|
||||||
|
await uow.CompleteAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task ApplyAsync(Guid? tenantId)
|
public async Task ApplyAsync(Guid? tenantId)
|
||||||
{
|
{
|
||||||
var scopeFolderName = SeedPathResolver.GetScopeFolderName(tenantId);
|
var scopeFolderName = SeedPathResolver.GetScopeFolderName(tenantId);
|
||||||
|
|
@ -87,7 +165,7 @@ public class ListFormSeedDataApplier(
|
||||||
// Seed contributor'larin unit of work'u kapandigi icin her dosya kendi
|
// Seed contributor'larin unit of work'u kapandigi icin her dosya 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);
|
await ApplyFileAsync(filePath, overwriteExisting: false);
|
||||||
await uow.CompleteAsync();
|
await uow.CompleteAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -126,7 +204,11 @@ public class ListFormSeedDataApplier(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ApplyFileAsync(string filePath)
|
/// <summary>
|
||||||
|
/// Dosyayi okur ve satirlari ilgili ListForm'un tablosuna uygular. Tek bir dosyanin hatasi
|
||||||
|
/// diger dosyalari etkilemez; hata sonuca tasinir ve loglanir.
|
||||||
|
/// </summary>
|
||||||
|
private async Task<ListFormSeedDataApplyResult> ApplyFileAsync(string filePath, bool overwriteExisting)
|
||||||
{
|
{
|
||||||
var fileName = Path.GetFileName(filePath);
|
var fileName = Path.GetFileName(filePath);
|
||||||
try
|
try
|
||||||
|
|
@ -142,14 +224,14 @@ public class ListFormSeedDataApplier(
|
||||||
if (seedFile?.Rows == null || seedFile.Rows.Count == 0)
|
if (seedFile?.Rows == null || seedFile.Rows.Count == 0)
|
||||||
{
|
{
|
||||||
logger.LogWarning("[{File}] No row found, skipped.", fileName);
|
logger.LogWarning("[{File}] No row found, skipped.", fileName);
|
||||||
return;
|
return new ListFormSeedDataApplyResult(fileName, 0, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
var listForm = await listFormRepository.FirstOrDefaultAsync(a => a.ListFormCode == listFormCode);
|
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("[{File}] ListForm '{ListFormCode}' not found, skipped.", fileName, listFormCode);
|
||||||
return;
|
return new ListFormSeedDataApplyResult(fileName, 0, 0, 0, $"ListForm '{listFormCode}' not found.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var fields = await listFormFieldRepository.GetListAsync(a => a.ListFormCode == listFormCode);
|
var fields = await listFormFieldRepository.GetListAsync(a => a.ListFormCode == listFormCode);
|
||||||
|
|
@ -160,38 +242,53 @@ public class ListFormSeedDataApplier(
|
||||||
var (repository, connectionString, dataSourceType) = await dynamicDataManager.GetAsync(listForm.IsTenant, listForm.DataSourceCode);
|
var (repository, connectionString, dataSourceType) = await dynamicDataManager.GetAsync(listForm.IsTenant, listForm.DataSourceCode);
|
||||||
|
|
||||||
var inserted = 0;
|
var inserted = 0;
|
||||||
|
var updated = 0;
|
||||||
var skipped = 0;
|
var skipped = 0;
|
||||||
|
|
||||||
foreach (var row in seedFile.Rows)
|
foreach (var row in seedFile.Rows)
|
||||||
{
|
{
|
||||||
if (await ApplyRowAsync(listForm, fields, keyFieldName, row, repository, connectionString, dataSourceType))
|
switch (await ApplyRowAsync(listForm, fields, keyFieldName, row, repository, connectionString, dataSourceType, overwriteExisting))
|
||||||
inserted++;
|
{
|
||||||
else
|
case SeedRowOutcome.Inserted:
|
||||||
skipped++;
|
inserted++;
|
||||||
|
break;
|
||||||
|
case SeedRowOutcome.Updated:
|
||||||
|
updated++;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
skipped++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.LogInformation("[{File}] {Inserted} row(s) inserted, {Skipped} existing row(s) skipped.", fileName, inserted, skipped);
|
logger.LogInformation(
|
||||||
|
"[{File}] {Inserted} row(s) inserted, {Updated} row(s) updated, {Skipped} existing row(s) skipped.",
|
||||||
|
fileName, inserted, updated, skipped);
|
||||||
|
return new ListFormSeedDataApplyResult(fileName, inserted, updated, skipped);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// Tek bir dosyanin hatasi diger listelerin seed edilmesini engellemez.
|
// Tek bir dosyanin 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 file: {FileName}", fileName);
|
||||||
|
return new ListFormSeedDataApplyResult(fileName, 0, 0, 0, ex.Message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Satiri ekler; kayit eklendiyse <c>true</c>, ayni kayit zaten varsa <c>false</c> doner.
|
/// Satiri veritabanina yazar. Kayit yoksa eklenir; varsa <paramref name="overwriteExisting"/>
|
||||||
|
/// true ise dosyadaki degerlerle guncellenir ve soft delete izleri temizlenir, degilse dokunulmaz.
|
||||||
/// Dosyada anahtar yoksa (identity kolonlu tablolar) satir diger alanlarinin tamami uzerinden
|
/// Dosyada anahtar yoksa (identity kolonlu tablolar) satir diger alanlarinin tamami uzerinden
|
||||||
/// eslesir ve anahtar veritabanina biraktirilir.
|
/// eslesir, anahtar veritabanina biraktirilir ve guncelleme yapilamaz.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task<bool> ApplyRowAsync(
|
private async Task<SeedRowOutcome> ApplyRowAsync(
|
||||||
ListForm listForm,
|
ListForm listForm,
|
||||||
List<ListFormField> fields,
|
List<ListFormField> fields,
|
||||||
string keyFieldName,
|
string keyFieldName,
|
||||||
Dictionary<string, object> row,
|
Dictionary<string, object> row,
|
||||||
IDynamicDataRepository repository,
|
IDynamicDataRepository repository,
|
||||||
string connectionString,
|
string connectionString,
|
||||||
DataSourceTypeEnum dataSourceType)
|
DataSourceTypeEnum dataSourceType,
|
||||||
|
bool overwriteExisting)
|
||||||
{
|
{
|
||||||
var keyField = fields.FirstOrDefault(a => a.FieldName == keyFieldName);
|
var keyField = fields.FirstOrDefault(a => a.FieldName == keyFieldName);
|
||||||
var keyValue = QueryHelper.GetFormattedValue(
|
var keyValue = QueryHelper.GetFormattedValue(
|
||||||
|
|
@ -201,9 +298,9 @@ public class ListFormSeedDataApplier(
|
||||||
object[] keys = [keyValue];
|
object[] keys = [keyValue];
|
||||||
var hasKey = keyValue != null;
|
var hasKey = keyValue != null;
|
||||||
|
|
||||||
var insertParameters = BuildParameters(fields, await ResolveTokensAsync(row), keyFieldName, includeKey: hasKey);
|
var resolvedRow = await ResolveTokensAsync(row);
|
||||||
|
var insertParameters = BuildParameters(fields, resolvedRow, keyFieldName, includeKey: hasKey);
|
||||||
|
|
||||||
// Var olan kayda dokunulmaz: veritabanindaki guncel deger, dosyadaki eski degerle ezilmemeli.
|
|
||||||
var match = hasKey
|
var match = hasKey
|
||||||
? new Dictionary<string, object> { [keyFieldName] = keyValue }
|
? new Dictionary<string, object> { [keyFieldName] = keyValue }
|
||||||
: new Dictionary<string, object>(insertParameters);
|
: new Dictionary<string, object>(insertParameters);
|
||||||
|
|
@ -215,7 +312,31 @@ public class ListFormSeedDataApplier(
|
||||||
|
|
||||||
if (await ExistsAsync(listForm, match, repository, connectionString))
|
if (await ExistsAsync(listForm, match, repository, connectionString))
|
||||||
{
|
{
|
||||||
return false;
|
// Anahtarsiz satirin hangi kayda karsilik geldigi bilinmedigi icin guncellenemez.
|
||||||
|
if (!overwriteExisting || !hasKey)
|
||||||
|
return SeedRowOutcome.Skipped;
|
||||||
|
|
||||||
|
// Anahtar SET listesine girmez; GenerateQuery onu WHERE icin parametrelere ekler.
|
||||||
|
var updateParameters = BuildParameters(fields, resolvedRow, keyFieldName, includeKey: false);
|
||||||
|
|
||||||
|
// Kayit daha once silinmis olabilir: satir tabloda durur ama ekranin varsayilan
|
||||||
|
// suzgeci ("IsDeleted" = 'false') onu gizler. Dosyadan gelen icerik kaydin var olmasi
|
||||||
|
// gerektigini soyledigi icin silinme izleri temizlenir, aksi halde import sessizce
|
||||||
|
// gorunmeyen bir kayit gunceller.
|
||||||
|
foreach (var reset in BuildSoftDeleteResetParameters(listForm))
|
||||||
|
{
|
||||||
|
if (!updateParameters.ContainsKey(reset.Key))
|
||||||
|
updateParameters[reset.Key] = reset.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateParameters.Count == 0)
|
||||||
|
return SeedRowOutcome.Skipped;
|
||||||
|
|
||||||
|
await MergeDefaultsAsync(listForm, fields, OperationEnum.Update, keys, updateParameters);
|
||||||
|
|
||||||
|
var updateSql = queryManager.GenerateQuery(listForm, fields, updateParameters, OperationEnum.Update, dataSourceType, keys);
|
||||||
|
await repository.ExecuteAsync(updateSql, connectionString, updateParameters);
|
||||||
|
return SeedRowOutcome.Updated;
|
||||||
}
|
}
|
||||||
|
|
||||||
await MergeDefaultsAsync(listForm, fields, OperationEnum.Insert, keys, insertParameters);
|
await MergeDefaultsAsync(listForm, fields, OperationEnum.Insert, keys, insertParameters);
|
||||||
|
|
@ -235,7 +356,32 @@ public class ListFormSeedDataApplier(
|
||||||
|
|
||||||
var insertSql = queryManager.GenerateQuery(listForm, fields, insertParameters, OperationEnum.Insert, dataSourceType, keys);
|
var insertSql = queryManager.GenerateQuery(listForm, fields, insertParameters, OperationEnum.Insert, dataSourceType, keys);
|
||||||
await repository.ExecuteAsync(insertSql, connectionString, insertParameters);
|
await repository.ExecuteAsync(insertSql, connectionString, insertParameters);
|
||||||
return true;
|
return SeedRowOutcome.Inserted;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ekranin silme komutunun yazdigi soft delete kolonlarini bulup onlari sifirlayan
|
||||||
|
/// parametreleri dondurur. Kolon listesi komuttan okunur; boylece yalnizca gercekten var olan
|
||||||
|
/// kolonlara yazilir ve donanimsal (hard) silme yapan bir ekranda hicbir sey eklenmez.
|
||||||
|
/// </summary>
|
||||||
|
private static Dictionary<string, object> BuildSoftDeleteResetParameters(ListForm listForm)
|
||||||
|
{
|
||||||
|
var command = listForm.DeleteCommand;
|
||||||
|
var parameters = new Dictionary<string, object>();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(command))
|
||||||
|
return parameters;
|
||||||
|
|
||||||
|
if (command.Contains("IsDeleted", StringComparison.OrdinalIgnoreCase))
|
||||||
|
parameters["IsDeleted"] = false;
|
||||||
|
|
||||||
|
if (command.Contains("DeletionTime", StringComparison.OrdinalIgnoreCase))
|
||||||
|
parameters["DeletionTime"] = null;
|
||||||
|
|
||||||
|
if (command.Contains("DeleterId", StringComparison.OrdinalIgnoreCase))
|
||||||
|
parameters["DeleterId"] = null;
|
||||||
|
|
||||||
|
return parameters;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Verilen alan degerleriyle eslesen bir kayit hedef tabloda var mi.</summary>
|
/// <summary>Verilen alan degerleriyle eslesen bir kayit hedef tabloda var mi.</summary>
|
||||||
|
|
|
||||||
|
|
@ -476,6 +476,7 @@ public static class SeedConsts
|
||||||
public const string IntranetWidgets = Default + ".IntranetWidgets";
|
public const string IntranetWidgets = Default + ".IntranetWidgets";
|
||||||
|
|
||||||
public const string DynamicServices = Default + ".DynamicServices";
|
public const string DynamicServices = Default + ".DynamicServices";
|
||||||
|
public const string CustomComponents = Default + ".CustomComponents";
|
||||||
}
|
}
|
||||||
|
|
||||||
public const string Forum = Prefix.App + ".Forum";
|
public const string Forum = Prefix.App + ".Forum";
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,6 @@ namespace Sozsoft.Platform.Data.Seeds;
|
||||||
/// seeds/
|
/// seeds/
|
||||||
/// host/ → host (varsayilan) veritabani kapsami
|
/// host/ → host (varsayilan) veritabani kapsami
|
||||||
/// crud/
|
/// crud/
|
||||||
/// custom/
|
|
||||||
/// wizard/
|
/// wizard/
|
||||||
/// data/
|
/// data/
|
||||||
/// sql/ → SQL Server saglayicisi
|
/// sql/ → SQL Server saglayicisi
|
||||||
|
|
@ -34,7 +33,7 @@ namespace Sozsoft.Platform.Data.Seeds;
|
||||||
/// execute/
|
/// execute/
|
||||||
/// tenants/
|
/// tenants/
|
||||||
/// {tenantId}/ → her tenant kendi klasorunde
|
/// {tenantId}/ → her tenant kendi klasorunde
|
||||||
/// crud/ custom/ wizard/ data/ sql/{object,execute}/ postgres/{object,execute}/
|
/// crud/ wizard/ data/ sql/{object,execute}/ postgres/{object,execute}/
|
||||||
/// </code>
|
/// </code>
|
||||||
/// </para>
|
/// </para>
|
||||||
/// <para>
|
/// <para>
|
||||||
|
|
@ -72,9 +71,6 @@ public static class SeedPathResolver
|
||||||
/// <summary>Crud endpoint konfigurasyon dosyalari.</summary>
|
/// <summary>Crud endpoint konfigurasyon dosyalari.</summary>
|
||||||
public const string CrudFolder = "crud";
|
public const string CrudFolder = "crud";
|
||||||
|
|
||||||
/// <summary>Custom component konfigurasyon dosyalari.</summary>
|
|
||||||
public const string CustomFolder = "custom";
|
|
||||||
|
|
||||||
/// <summary>Liste verisinin aynalandigi seed dosyalari (<c>data/{ListFormCode}.json</c>).</summary>
|
/// <summary>Liste verisinin aynalandigi seed dosyalari (<c>data/{ListFormCode}.json</c>).</summary>
|
||||||
public const string DataFolder = "data";
|
public const string DataFolder = "data";
|
||||||
|
|
||||||
|
|
|
||||||
20
claude.md
20
claude.md
|
|
@ -184,15 +184,21 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` →
|
||||||
`permission.constant.ts` + `LanguagesData.json`.
|
`permission.constant.ts` + `LanguagesData.json`.
|
||||||
- **Yeni bir dil anahtarı** → `LanguagesData.json`; eklemeden önce anahtarın zaten var olup
|
- **Yeni bir dil anahtarı** → `LanguagesData.json`; eklemeden önce anahtarın zaten var olup
|
||||||
olmadığını kontrol et.
|
olmadığını kontrol et.
|
||||||
- **Custom component kaydı/silinmesi** → `custom/{Name}.json`; kullandığı endpoint'ler
|
- **Custom component kaydı/silinmesi** → `data/App.DeveloperKit.CustomComponents.json`
|
||||||
`crud/{Entity}.json`.
|
(Custom Components ekranının seed verisi); bileşenin kendi dosyası yoktur, her bileşen bu
|
||||||
- **Wizard dosyası** → tek başına taşınmaz; export zip'i `wizard/`, `custom/`, `crud/` ve
|
dosyada bir satırdır. Hem listeden hem Visual Designer'dan (`CustomComponentAppService`) yapılan
|
||||||
`{sql|postgres}/{object|execute}/` bağımlılıklarını da içerir.
|
kayıt aynı dosyaya yansır; kullandığı endpoint'ler `crud/{Entity}.json`.
|
||||||
|
- **Wizard dosyası** → tek başına taşınmaz; export zip'i `wizard/`, `data/`, `crud/` ve
|
||||||
|
`{sql|postgres}/{object|execute}/` bağımlılıklarını da içerir. `Custom` yolunda `data/` girdisi
|
||||||
|
yalnızca ilgili bileşen ve bağımlılık zincirine süzülür; import'ta hedef dosyanın üstüne
|
||||||
|
yazılmaz, satırlar anahtar üzerinden birleştirilir.
|
||||||
- **`ListForm.SeedFilePath` dolu bir ekran** → `SeedSyncInsert/SeedSyncUpdate/SeedSyncDelete`
|
- **`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 `data/{ListFormCode}.json` dosyasına
|
||||||
yansır (`ListFormSeedDataSynchronizer`); migrate/seed sırasında `ListFormSeedDataApplier` dosyayı
|
yansır (`ListFormSeedDataSynchronizer`); migrate/seed sırasında ve wizard import'unun kapanışında
|
||||||
geri uygular (kayıt yoksa ekler, varsa dokunmaz). Yeni bir kayıt yolu (yeni endpoint, toplu işlem)
|
(`CompleteImport`) `ListFormSeedDataApplier` dosyayı geri uygular. Migrate/seed'de kayıt yoksa
|
||||||
eklenirse senkronizasyon çağrısı da eklenir; yoksa dosya bayatlar.
|
eklenir, varsa dokunulmaz; import'ta `overwriteExisting` açıktır: var olan kayıt dosyadaki
|
||||||
|
değerlerle güncellenir ve soft delete izleri (`IsDeleted`/`DeletionTime`/`DeleterId`) temizlenir. Yeni bir kayıt yolu (yeni endpoint, toplu işlem, tipli AppService) eklenirse
|
||||||
|
senkronizasyon çağrısı da eklenir; yoksa dosya bayatlar.
|
||||||
- **`MenusData.json`** → `Routes` bölümüne dokunma; yalnızca `Modules` ve `Menus`.
|
- **`MenusData.json`** → `Routes` bölümüne dokunma; yalnızca `Modules` ve `Menus`.
|
||||||
- **Çok değerli alan (`dxTagBox` / çoklu `dxGridBox`)** → tek metin kolonunda `|` ile saklanır.
|
- **Çok değerli alan (`dxTagBox` / çoklu `dxGridBox`)** → tek metin kolonunda `|` ile saklanır.
|
||||||
Ekran varsayılan `list-form-data/*` ucundan kaydediyorsa birleştirmeyi `QueryHelper` yapar;
|
Ekran varsayılan `list-form-data/*` ucundan kaydediyorsa birleştirmeyi `QueryHelper` yapar;
|
||||||
|
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
{
|
|
||||||
"GeneratedAt": "2026-08-20T11:16:38.2313189Z",
|
|
||||||
"CustomComponents": [
|
|
||||||
{
|
|
||||||
"Name": "DynamicEntityComponent",
|
|
||||||
"RoutePath": "/admin/dynamic-entity",
|
|
||||||
"Code": "import React, { useEffect, useState } from \"react\";\nimport axios from \"axios\";\n\ninterface DynamicEntityComponentProps {\n title: string;\n}\n\nconst api = axios.create({\n baseURL: \"https://localhost:44344/\",\n});\n\nconst DynamicEntityComponent: React.FC<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,
|
|
||||||
"Description": null,
|
|
||||||
"IsActive": true,
|
|
||||||
"Dependencies": [],
|
|
||||||
"DataSources": []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
{
|
|
||||||
"GeneratedAt": "2026-08-20T11:23:45.7859641Z",
|
|
||||||
"CustomComponents": [
|
|
||||||
{
|
|
||||||
"Name": "NewComponent",
|
|
||||||
"RoutePath": "/admin/new-component",
|
|
||||||
"Code": "/*__SOZSOFT_VISUAL_DESIGNER__%7B%22version%22%3A1%2C%22sourceMode%22%3A%22visual%22%2C%22nodes%22%3A%5B%5D%2C%22canvas%22%3A%7B%22width%22%3A%22responsive%22%7D%2C%22lifecycle%22%3A%7B%22onMount%22%3A%22%22%7D%2C%22dataSources%22%3A%5B%5D%7D__*/\nconst NewComponent = () => {\n\n\n return (\n <>\n\n </>\n )\n}\n\nexport default NewComponent",
|
|
||||||
"Props": "{\"visualDesigner\":{\"version\":1,\"sourceMode\":\"visual\",\"nodes\":[],\"canvas\":{\"width\":\"responsive\"},\"lifecycle\":{\"onMount\":\"\"},\"dataSources\":[]}}",
|
|
||||||
"Description": "",
|
|
||||||
"IsActive": true,
|
|
||||||
"Dependencies": [],
|
|
||||||
"DataSources": []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
{
|
|
||||||
"GeneratedAt": "2026-08-18T00:00:00Z",
|
|
||||||
"CustomComponents": [
|
|
||||||
{
|
|
||||||
"Name": "RoleListComponent",
|
|
||||||
"RoutePath": "/admin/roles-list",
|
|
||||||
"Code": "const RoleListComponent = ({\n title = \"AbpRoles\"\n}) => {\n return (\n <DynamicEntityComponent id=\"c_mdljvvmq_fno52v\" title={title} />\n );\n};\n\nexport default RoleListComponent;",
|
|
||||||
"Props": null,
|
|
||||||
"Description": null,
|
|
||||||
"IsActive": true,
|
|
||||||
"Dependencies": [
|
|
||||||
"DynamicEntityComponent"
|
|
||||||
],
|
|
||||||
"DataSources": []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
File diff suppressed because one or more lines are too long
159
configs/seeds/host/wizard/RoleList.json
Normal file
159
configs/seeds/host/wizard/RoleList.json
Normal file
|
|
@ -0,0 +1,159 @@
|
||||||
|
{
|
||||||
|
"Wizard": {
|
||||||
|
"ComponentKind": 1,
|
||||||
|
"WizardName": "RoleList",
|
||||||
|
"WizardCode": "App.Wizard.RoleList",
|
||||||
|
"SeededAt": "2026-09-05T19:53:59.1764998\u002B03:00",
|
||||||
|
"CustomComponentName": "RoleComponent",
|
||||||
|
"PermissionGroupName": "App.Wizard.Sales",
|
||||||
|
"ModuleId": "Sal",
|
||||||
|
"Menu": {
|
||||||
|
"MenuParentCode": "App.Wizard.Sales",
|
||||||
|
"MenuParentIcon": "FcAssistant",
|
||||||
|
"MenuCode": "App.Wizard.RoleList",
|
||||||
|
"MenuOrder": 2,
|
||||||
|
"MenuUrl": "/admin/roles",
|
||||||
|
"MenuIcon": "FcBullish"
|
||||||
|
},
|
||||||
|
"Language": {
|
||||||
|
"MenuParentEn": "Sales",
|
||||||
|
"MenuParentTr": "Sat\u0131\u015F",
|
||||||
|
"PermissionGroupEn": "Sales",
|
||||||
|
"PermissionGroupTr": "Sat\u0131\u015F",
|
||||||
|
"MenuEn": "Role List",
|
||||||
|
"MenuTr": "Role List",
|
||||||
|
"TitleEn": "Role List",
|
||||||
|
"TitleTr": "Role List",
|
||||||
|
"DescEn": "Role List",
|
||||||
|
"DescTr": "Role List"
|
||||||
|
},
|
||||||
|
"Groups": [],
|
||||||
|
"IsTenant": false,
|
||||||
|
"IsBranch": false,
|
||||||
|
"IsOrganizationUnit": false,
|
||||||
|
"AllowAdding": true,
|
||||||
|
"AllowUpdating": true,
|
||||||
|
"AllowDeleting": true,
|
||||||
|
"AllowDetail": false,
|
||||||
|
"ConfirmDelete": true,
|
||||||
|
"DefaultLayout": "grid",
|
||||||
|
"Grid": true,
|
||||||
|
"Card": true,
|
||||||
|
"Pivot": true,
|
||||||
|
"Tree": false,
|
||||||
|
"Chart": true,
|
||||||
|
"Gantt": false,
|
||||||
|
"Scheduler": false,
|
||||||
|
"Todo": false,
|
||||||
|
"DataSourceCode": "Default",
|
||||||
|
"SelectCommandType": 1,
|
||||||
|
"SelectCommand": "",
|
||||||
|
"KeyFieldName": "",
|
||||||
|
"KeyFieldDbSourceType": 11,
|
||||||
|
"TreeOptionDto": {
|
||||||
|
"KeyExpr": "",
|
||||||
|
"ParentIdExpr": "",
|
||||||
|
"HasItemsExpr": "",
|
||||||
|
"RootValue": "",
|
||||||
|
"ExpandedRowKeys": [],
|
||||||
|
"AutoExpandAll": false,
|
||||||
|
"RecursiveSelection": false,
|
||||||
|
"TitleExpr": null,
|
||||||
|
"StartExpr": null,
|
||||||
|
"EndExpr": null,
|
||||||
|
"ProgressExpr": null
|
||||||
|
},
|
||||||
|
"GanttOptionDto": {
|
||||||
|
"KeyExpr": "",
|
||||||
|
"ParentIdExpr": "",
|
||||||
|
"HasItemsExpr": "",
|
||||||
|
"RootValue": "",
|
||||||
|
"ExpandedRowKeys": [],
|
||||||
|
"AutoExpandAll": false,
|
||||||
|
"RecursiveSelection": false,
|
||||||
|
"TitleExpr": "",
|
||||||
|
"ScaleType": "weeks",
|
||||||
|
"StartExpr": "",
|
||||||
|
"EndExpr": "",
|
||||||
|
"ProgressExpr": "",
|
||||||
|
"EditingEnabled": false,
|
||||||
|
"AllowTaskAdding": false,
|
||||||
|
"AllowTaskUpdating": false,
|
||||||
|
"AllowTaskResourceUpdating": false,
|
||||||
|
"AllowTaskDeleting": false,
|
||||||
|
"AllowDependencyAdding": false,
|
||||||
|
"AllowDependencyDeleting": false,
|
||||||
|
"AllowResourceAdding": false,
|
||||||
|
"AllowResourceUpdating": false,
|
||||||
|
"AllowResourceDeleting": false
|
||||||
|
},
|
||||||
|
"SchedulerOptionDto": {
|
||||||
|
"TextExpr": "",
|
||||||
|
"UserNameExpr": null,
|
||||||
|
"DescriptionExpr": null,
|
||||||
|
"StartDateExpr": "",
|
||||||
|
"EndDateExpr": "",
|
||||||
|
"AllDayExpr": "",
|
||||||
|
"RecurrenceRuleExpr": "",
|
||||||
|
"RecurrenceExceptionExpr": "",
|
||||||
|
"StartDayHour": 8,
|
||||||
|
"EndDayHour": 20,
|
||||||
|
"DefaultView": "week",
|
||||||
|
"ShowAllDayPanel": true,
|
||||||
|
"CellDuration": 30,
|
||||||
|
"FirstDayOfWeek": 1,
|
||||||
|
"CrossScrollingEnabled": false,
|
||||||
|
"AllowResizing": false,
|
||||||
|
"AllowDragging": false,
|
||||||
|
"AllowDeleting": false,
|
||||||
|
"AllowEditing": false,
|
||||||
|
"AllowAdding": false
|
||||||
|
},
|
||||||
|
"TodoOptionDto": {
|
||||||
|
"TitleExpr": "",
|
||||||
|
"StatusExpr": "",
|
||||||
|
"DescriptionExpr": "",
|
||||||
|
"DueDateExpr": "",
|
||||||
|
"TagExpr": "",
|
||||||
|
"AssigneeExpr": "",
|
||||||
|
"PriorityExpr": "",
|
||||||
|
"CompletedExpr": "",
|
||||||
|
"OrderExpr": "",
|
||||||
|
"TimesheetExpr": null,
|
||||||
|
"SubTaskExpr": null,
|
||||||
|
"StatusOrder": "Backlog, Ready to Start, In Progress, Code Review, Testing, Staging",
|
||||||
|
"AllowDragging": true
|
||||||
|
},
|
||||||
|
"WorkflowDto": {
|
||||||
|
"ApprovalUserFieldName": "",
|
||||||
|
"ApprovalDateFieldName": "",
|
||||||
|
"ApprovalStatusFieldName": "",
|
||||||
|
"ApprovalDescriptionFieldName": "",
|
||||||
|
"ApprovalIsFilterUserName": false,
|
||||||
|
"ApprovalIsResetWorkflow": false,
|
||||||
|
"Criteria": []
|
||||||
|
},
|
||||||
|
"SubForms": [],
|
||||||
|
"Widgets": []
|
||||||
|
},
|
||||||
|
"ListForm": null,
|
||||||
|
"Fields": [],
|
||||||
|
"IsDeletedField": false,
|
||||||
|
"IsCreatedField": false,
|
||||||
|
"InsertedRecords": {
|
||||||
|
"LanguageKeys": [
|
||||||
|
"App.Wizard.RoleList"
|
||||||
|
],
|
||||||
|
"PermissionGroupNames": [],
|
||||||
|
"PermissionNames": [
|
||||||
|
"App.Wizard.RoleList",
|
||||||
|
"App.Wizard.RoleList.Create",
|
||||||
|
"App.Wizard.RoleList.Update",
|
||||||
|
"App.Wizard.RoleList.Delete"
|
||||||
|
],
|
||||||
|
"MenuCodes": [
|
||||||
|
"App.Wizard.RoleList"
|
||||||
|
],
|
||||||
|
"DataSourceCodes": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
import type { PropsWithChildren } from 'react'
|
|
||||||
import { Container } from '../shared'
|
|
||||||
|
|
||||||
const DeveloperLayout = ({ children }: PropsWithChildren) => {
|
|
||||||
return (
|
|
||||||
<Container>
|
|
||||||
<div className="flex flex-col lg:flex-row gap-8">
|
|
||||||
<div className="flex-1">{children}</div>
|
|
||||||
</div>
|
|
||||||
</Container>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DeveloperLayout
|
|
||||||
|
|
@ -31,19 +31,6 @@ export const WIZARD_PERMISSION = {
|
||||||
IMPORT: 'App.Listforms.Wizard.Import',
|
IMPORT: 'App.Listforms.Wizard.Import',
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
/**
|
|
||||||
* Custom component yöneticisi yetkileri. `PermissionsData.json` içinde tanımlıdır ve seeder ile
|
|
||||||
* yönetilir; buradaki kontroller yalnızca butonları gizler, asıl kontrol
|
|
||||||
* `CustomComponentAppService` üzerindeki ABP policy adlarındadır.
|
|
||||||
*/
|
|
||||||
export const COMPONENT_PERMISSION = {
|
|
||||||
/** Ekranı görme (okuma) yetkisi; alt yetkilerin tamamının üstüdür. */
|
|
||||||
DEFAULT: 'App.DeveloperKit.Components',
|
|
||||||
CREATE: 'App.DeveloperKit.Components.Create',
|
|
||||||
UPDATE: 'App.DeveloperKit.Components.Update',
|
|
||||||
DELETE: 'App.DeveloperKit.Components.Delete',
|
|
||||||
} as const
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ListForm alan (Fields) yoneticisi yetkileri. `PermissionsData.json` icinde tanimlidir ve
|
* ListForm alan (Fields) yoneticisi yetkileri. `PermissionsData.json` icinde tanimlidir ve
|
||||||
* seeder ile yonetilir; buradaki kontroller yalnizca butonlari kapatir, asil kontrol
|
* seeder ile yonetilir; buradaki kontroller yalnizca butonlari kapatir, asil kontrol
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,6 @@ import {
|
||||||
buildRuntimeScope,
|
buildRuntimeScope,
|
||||||
compileComponentBundle,
|
compileComponentBundle,
|
||||||
getCompilationSignature,
|
getCompilationSignature,
|
||||||
toErrorMessage,
|
|
||||||
type BabelLike,
|
type BabelLike,
|
||||||
type RuntimeComponent,
|
type RuntimeComponent,
|
||||||
} from './componentRuntime'
|
} from './componentRuntime'
|
||||||
|
|
@ -34,19 +33,12 @@ interface ComponentProps {
|
||||||
|
|
||||||
interface ComponentContextType {
|
interface ComponentContextType {
|
||||||
components: CustomComponent[]
|
components: CustomComponent[]
|
||||||
loading: boolean
|
|
||||||
error: string | null
|
|
||||||
/** True while the runtime bundle is being (re)compiled. */
|
/** True while the runtime bundle is being (re)compiled. */
|
||||||
compiling: boolean
|
compiling: boolean
|
||||||
/** Compilation/initialization errors keyed by component name. */
|
/** Compilation/initialization errors keyed by component name. */
|
||||||
componentErrors: Record<string, string>
|
componentErrors: Record<string, string>
|
||||||
addComponent: (component: CreateUpdateCustomComponentDto) => Promise<void>
|
|
||||||
updateComponent: (id: string, component: CreateUpdateCustomComponentDto) => Promise<void>
|
updateComponent: (id: string, component: CreateUpdateCustomComponentDto) => Promise<void>
|
||||||
deleteComponent: (id: string) => Promise<void>
|
|
||||||
getComponent: (id: string) => CustomComponent | undefined
|
|
||||||
getComponentByName: (name: string) => CustomComponent | undefined
|
getComponentByName: (name: string) => CustomComponent | undefined
|
||||||
refreshComponents: () => Promise<void>
|
|
||||||
refreshComponent: (id: string) => Promise<CustomComponent | undefined>
|
|
||||||
/** Natively implemented components exposed to dynamic routes by name. */
|
/** Natively implemented components exposed to dynamic routes by name. */
|
||||||
registeredComponents: Record<string, React.ComponentType<unknown>>
|
registeredComponents: Record<string, React.ComponentType<unknown>>
|
||||||
registerComponent: (name: string, component: React.ComponentType<unknown>) => void
|
registerComponent: (name: string, component: React.ComponentType<unknown>) => void
|
||||||
|
|
@ -103,8 +95,6 @@ export const ComponentProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
||||||
const extraProperties = applicationConfig?.extraProperties
|
const extraProperties = applicationConfig?.extraProperties
|
||||||
|
|
||||||
const [components, setComponents] = useState<CustomComponent[]>([])
|
const [components, setComponents] = useState<CustomComponent[]>([])
|
||||||
const [loading, setLoading] = useState(false)
|
|
||||||
const [error, setError] = useState<string | null>(null)
|
|
||||||
const [compiling, setCompiling] = useState(false)
|
const [compiling, setCompiling] = useState(false)
|
||||||
const [registeredComponents, setRegisteredComponents] =
|
const [registeredComponents, setRegisteredComponents] =
|
||||||
useState<Record<string, React.ComponentType<unknown>>>(EMPTY_NATIVE_REGISTRY)
|
useState<Record<string, React.ComponentType<unknown>>>(EMPTY_NATIVE_REGISTRY)
|
||||||
|
|
@ -145,41 +135,16 @@ export const ComponentProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
||||||
if (!applicationConfig) return
|
if (!applicationConfig) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setError(null)
|
|
||||||
const result = await developerKitService.getCustomComponents()
|
const result = await developerKitService.getCustomComponents()
|
||||||
const nextComponents = result.items || []
|
const nextComponents = result.items || []
|
||||||
setComponents((current) =>
|
setComponents((current) =>
|
||||||
haveSameComponentRevision(current, nextComponents) ? current : nextComponents,
|
haveSameComponentRevision(current, nextComponents) ? current : nextComponents,
|
||||||
)
|
)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(toErrorMessage(err))
|
|
||||||
console.error('Failed to fetch components:', err)
|
console.error('Failed to fetch components:', err)
|
||||||
}
|
}
|
||||||
}, [applicationConfig])
|
}, [applicationConfig])
|
||||||
|
|
||||||
const refreshComponent = useCallback(
|
|
||||||
async (id: string): Promise<CustomComponent | undefined> => {
|
|
||||||
if (!applicationConfig) return undefined
|
|
||||||
|
|
||||||
try {
|
|
||||||
setError(null)
|
|
||||||
const freshComponent = await developerKitService.getCustomComponent(id)
|
|
||||||
setComponents((current) => {
|
|
||||||
const exists = current.some((component) => component.id === id)
|
|
||||||
return exists
|
|
||||||
? current.map((component) => (component.id === id ? freshComponent : component))
|
|
||||||
: [...current, freshComponent]
|
|
||||||
})
|
|
||||||
return freshComponent
|
|
||||||
} catch (err) {
|
|
||||||
setError(toErrorMessage(err))
|
|
||||||
console.error('Failed to fetch component:', err)
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[applicationConfig],
|
|
||||||
)
|
|
||||||
|
|
||||||
// Components delivered with the application configuration are the initial set.
|
// Components delivered with the application configuration are the initial set.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const initialComponents =
|
const initialComponents =
|
||||||
|
|
@ -197,60 +162,17 @@ export const ComponentProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
||||||
return () => componentsChangedChannel?.removeEventListener('message', handleComponentsChanged)
|
return () => componentsChangedChannel?.removeEventListener('message', handleComponentsChanged)
|
||||||
}, [refreshComponents])
|
}, [refreshComponents])
|
||||||
|
|
||||||
const addComponent = useCallback(async (componentData: CreateUpdateCustomComponentDto) => {
|
|
||||||
try {
|
|
||||||
setLoading(true)
|
|
||||||
setError(null)
|
|
||||||
const newComponent = await developerKitService.createCustomComponent(componentData)
|
|
||||||
setComponents((prev) => [...prev, newComponent])
|
|
||||||
notifyComponentsChanged()
|
|
||||||
} catch (err) {
|
|
||||||
setError(toErrorMessage(err))
|
|
||||||
throw err
|
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const updateComponent = useCallback(
|
const updateComponent = useCallback(
|
||||||
async (id: string, componentData: CreateUpdateCustomComponentDto) => {
|
async (id: string, componentData: CreateUpdateCustomComponentDto) => {
|
||||||
try {
|
const updatedComponent = await developerKitService.updateCustomComponent(id, componentData)
|
||||||
setLoading(true)
|
setComponents((prev) =>
|
||||||
setError(null)
|
prev.map((component) => (component.id === id ? updatedComponent : component)),
|
||||||
const updatedComponent = await developerKitService.updateCustomComponent(id, componentData)
|
)
|
||||||
setComponents((prev) =>
|
notifyComponentsChanged()
|
||||||
prev.map((component) => (component.id === id ? updatedComponent : component)),
|
|
||||||
)
|
|
||||||
notifyComponentsChanged()
|
|
||||||
} catch (err) {
|
|
||||||
setError(toErrorMessage(err))
|
|
||||||
throw err
|
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[],
|
[],
|
||||||
)
|
)
|
||||||
|
|
||||||
const deleteComponent = useCallback(async (id: string) => {
|
|
||||||
try {
|
|
||||||
setLoading(true)
|
|
||||||
setError(null)
|
|
||||||
await developerKitService.deleteCustomComponent(id)
|
|
||||||
setComponents((prev) => prev.filter((component) => component.id !== id))
|
|
||||||
notifyComponentsChanged()
|
|
||||||
} catch (err) {
|
|
||||||
setError(toErrorMessage(err))
|
|
||||||
throw err
|
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const componentsById = useMemo(
|
|
||||||
() => new Map(components.map((component) => [component.id, component])),
|
|
||||||
[components],
|
|
||||||
)
|
|
||||||
const componentsByName = useMemo(
|
const componentsByName = useMemo(
|
||||||
() => new Map(components.map((component) => [component.name, component])),
|
() => new Map(components.map((component) => [component.name, component])),
|
||||||
[components],
|
[components],
|
||||||
|
|
@ -271,8 +193,6 @@ export const ComponentProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
||||||
[compilableComponents],
|
[compilableComponents],
|
||||||
)
|
)
|
||||||
|
|
||||||
const getComponent = useCallback((id: string) => componentsById.get(id), [componentsById])
|
|
||||||
|
|
||||||
const getComponentByName = useCallback(
|
const getComponentByName = useCallback(
|
||||||
(name: string) => componentsByName.get(name),
|
(name: string) => componentsByName.get(name),
|
||||||
[componentsByName],
|
[componentsByName],
|
||||||
|
|
@ -397,17 +317,10 @@ export const ComponentProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
||||||
const value = useMemo<ComponentContextType>(
|
const value = useMemo<ComponentContextType>(
|
||||||
() => ({
|
() => ({
|
||||||
components,
|
components,
|
||||||
loading,
|
|
||||||
error,
|
|
||||||
compiling,
|
compiling,
|
||||||
componentErrors,
|
componentErrors,
|
||||||
addComponent,
|
|
||||||
updateComponent,
|
updateComponent,
|
||||||
deleteComponent,
|
|
||||||
getComponent,
|
|
||||||
getComponentByName,
|
getComponentByName,
|
||||||
refreshComponents,
|
|
||||||
refreshComponent,
|
|
||||||
registeredComponents,
|
registeredComponents,
|
||||||
registerComponent,
|
registerComponent,
|
||||||
renderComponent,
|
renderComponent,
|
||||||
|
|
@ -415,17 +328,10 @@ export const ComponentProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
components,
|
components,
|
||||||
loading,
|
|
||||||
error,
|
|
||||||
compiling,
|
compiling,
|
||||||
componentErrors,
|
componentErrors,
|
||||||
addComponent,
|
|
||||||
updateComponent,
|
updateComponent,
|
||||||
deleteComponent,
|
|
||||||
getComponent,
|
|
||||||
getComponentByName,
|
getComponentByName,
|
||||||
refreshComponents,
|
|
||||||
refreshComponent,
|
|
||||||
registeredComponents,
|
registeredComponents,
|
||||||
registerComponent,
|
registerComponent,
|
||||||
renderComponent,
|
renderComponent,
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -192,7 +192,7 @@ export enum WizardImportEntryStatusEnum {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WizardImportEntryDto {
|
export interface WizardImportEntryDto {
|
||||||
/** Kapsam klasorune goreli yol, ornegin `custom/Orders.json`. */
|
/** Kapsam klasorune goreli yol, ornegin `crud/Orders.json`. */
|
||||||
path: string
|
path: string
|
||||||
folder: string
|
folder: string
|
||||||
fileName: string
|
fileName: string
|
||||||
|
|
@ -213,6 +213,28 @@ export interface WizardImportAnalysisDto {
|
||||||
warnings: string[]
|
warnings: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import kapanisinin sonucu. Yazilan `data/*.json` dosyalari ayni islemde veritabanina da
|
||||||
|
* uygulanir; her dosyanin sonucu burada raporlanir.
|
||||||
|
*/
|
||||||
|
export interface WizardImportCompleteResultDto {
|
||||||
|
appliedData: WizardImportSeedApplyResultDto[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Tek bir veri seed dosyasinin veritabanina uygulanma sonucu. */
|
||||||
|
export interface WizardImportSeedApplyResultDto {
|
||||||
|
/** Kapsam klasorune goreli yol. */
|
||||||
|
path: string
|
||||||
|
/** Veritabanina eklenen satir sayisi. */
|
||||||
|
insertedRowCount: number
|
||||||
|
/** Zaten var oldugu icin dosyadaki degerlerle guncellenen satir sayisi. */
|
||||||
|
updatedRowCount: number
|
||||||
|
/** Anahtari olmadigi icin guncellenemeyen, dokunulmayan satir sayisi. */
|
||||||
|
skippedRowCount: number
|
||||||
|
/** Dosya uygulanamadiysa nedeni; basarili durumda bostur. */
|
||||||
|
error?: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface WizardImportApplyResultDto {
|
export interface WizardImportApplyResultDto {
|
||||||
path: string
|
path: string
|
||||||
overwritten: boolean
|
overwritten: boolean
|
||||||
|
|
|
||||||
|
|
@ -39,8 +39,7 @@
|
||||||
entities: '/admin/developerkit/entities',
|
entities: '/admin/developerkit/entities',
|
||||||
entitiesNew: '/admin/developerkit/entities/new',
|
entitiesNew: '/admin/developerkit/entities/new',
|
||||||
entitiesEdit: '/admin/developerkit/entities/edit/:id',
|
entitiesEdit: '/admin/developerkit/entities/edit/:id',
|
||||||
components: '/admin/developerkit/components',
|
components: '/admin/list/App.DeveloperKit.CustomComponents',
|
||||||
componentsEdit: '/admin/developerkit/components/edit/:id',
|
|
||||||
},
|
},
|
||||||
reports: {
|
reports: {
|
||||||
view: '/admin/reports/:report/view/:id?/:listFormCode?',
|
view: '/admin/reports/:report/view/:id?/:listFormCode?',
|
||||||
|
|
|
||||||
|
|
@ -18,14 +18,6 @@ class DeveloperKitService {
|
||||||
return response.data
|
return response.data
|
||||||
}
|
}
|
||||||
|
|
||||||
async getActiveCustomComponents(): Promise<PagedResultDto<CustomComponentDto>> {
|
|
||||||
const response = await apiService.fetchData<PagedResultDto<CustomComponentDto>>({
|
|
||||||
url: '/api/app/custom-component/active-components',
|
|
||||||
method: 'GET',
|
|
||||||
})
|
|
||||||
return response.data
|
|
||||||
}
|
|
||||||
|
|
||||||
async getCustomComponent(id: string): Promise<CustomComponent> {
|
async getCustomComponent(id: string): Promise<CustomComponent> {
|
||||||
const response = await apiService.fetchData<CustomComponent>({
|
const response = await apiService.fetchData<CustomComponent>({
|
||||||
url: `/api/app/custom-component/${id}`,
|
url: `/api/app/custom-component/${id}`,
|
||||||
|
|
@ -34,15 +26,6 @@ class DeveloperKitService {
|
||||||
return response.data
|
return response.data
|
||||||
}
|
}
|
||||||
|
|
||||||
async createCustomComponent(component: CreateUpdateCustomComponentDto): Promise<CustomComponent> {
|
|
||||||
const response = await apiService.fetchData<CustomComponent>({
|
|
||||||
url: '/api/app/custom-component',
|
|
||||||
method: 'POST',
|
|
||||||
data: component as any,
|
|
||||||
})
|
|
||||||
return response.data
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateCustomComponent(
|
async updateCustomComponent(
|
||||||
id: string,
|
id: string,
|
||||||
component: CreateUpdateCustomComponentDto,
|
component: CreateUpdateCustomComponentDto,
|
||||||
|
|
@ -59,13 +42,6 @@ class DeveloperKitService {
|
||||||
return response.data
|
return response.data
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteCustomComponent(id: string): Promise<void> {
|
|
||||||
await apiService.fetchData<void>({
|
|
||||||
url: `/api/app/custom-component/${id}`,
|
|
||||||
method: 'DELETE',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generated Endpoint endpoints
|
// Generated Endpoint endpoints
|
||||||
async toggleGeneratedEndpoint(id: string): Promise<CrudEndpoint> {
|
async toggleGeneratedEndpoint(id: string): Promise<CrudEndpoint> {
|
||||||
const response = await apiService.fetchData<CrudEndpoint>({
|
const response = await apiService.fetchData<CrudEndpoint>({
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import {
|
||||||
WizardFileInfoDto,
|
WizardFileInfoDto,
|
||||||
WizardImportAnalysisDto,
|
WizardImportAnalysisDto,
|
||||||
WizardImportApplyResultDto,
|
WizardImportApplyResultDto,
|
||||||
|
WizardImportCompleteResultDto,
|
||||||
WizardPermissionGroupDto,
|
WizardPermissionGroupDto,
|
||||||
WizardSeedFileDto,
|
WizardSeedFileDto,
|
||||||
} from '@/proxy/admin/wizard/models'
|
} from '@/proxy/admin/wizard/models'
|
||||||
|
|
@ -43,7 +44,7 @@ export const deleteWizardFile = (fileName: string) =>
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wizard dosyasini ve bagli seed dosyalarini (custom/crud/sql) tek bir zip olarak indirir.
|
* Wizard dosyasini ve bagli seed dosyalarini (data/crud/sql) tek bir zip olarak indirir.
|
||||||
* Zip icindeki klasor duzeni seed kapsam klasoru ile aynidir; baska bir ortamda dogrudan acilabilir.
|
* Zip icindeki klasor duzeni seed kapsam klasoru ile aynidir; baska bir ortamda dogrudan acilabilir.
|
||||||
*/
|
*/
|
||||||
export const exportWizardFile = (fileName: string) =>
|
export const exportWizardFile = (fileName: string) =>
|
||||||
|
|
@ -93,9 +94,12 @@ export const applyWizardImport = (importId: string, path: string, content?: stri
|
||||||
data: { importId, path, content: content ?? null },
|
data: { importId, path, content: content ?? null },
|
||||||
})
|
})
|
||||||
|
|
||||||
/** Import oturumunu kapatir; staging ve yedek klasorleri silinir. */
|
/**
|
||||||
|
* Import oturumunu kapatir; staging ve yedek klasorleri silinir. Kapanistan once yazilan
|
||||||
|
* `data/*.json` dosyalari veritabanina uygulanir ve sonucu doner.
|
||||||
|
*/
|
||||||
export const completeWizardImport = (importId: string) =>
|
export const completeWizardImport = (importId: string) =>
|
||||||
apiService.fetchData({
|
apiService.fetchData<WizardImportCompleteResultDto>({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: `/api/app/list-form-wizard/complete-import`,
|
url: `/api/app/list-form-wizard/complete-import`,
|
||||||
params: { importId },
|
params: { importId },
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ import { useNavigate } from 'react-router-dom'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import { Button, Dialog, Input, Notification, toast } from '@/components/ui'
|
import { Button, Dialog, Input, Notification, toast } from '@/components/ui'
|
||||||
import Container from '@/components/shared/Container'
|
import Container from '@/components/shared/Container'
|
||||||
|
import Loading from '@/components/shared/Loading'
|
||||||
|
import PageTitle from '@/components/shared/PageTitle'
|
||||||
import {
|
import {
|
||||||
FaTrash,
|
FaTrash,
|
||||||
FaSync,
|
FaSync,
|
||||||
|
|
@ -234,6 +236,8 @@ const WizardFileManager = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
|
<PageTitle title={translate('::App.Listforms.WizardManager')} />
|
||||||
|
|
||||||
{/* ── Header ─────────────────────────────────────────────── */}
|
{/* ── Header ─────────────────────────────────────────────── */}
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
|
|
@ -268,6 +272,7 @@ const WizardFileManager = () => {
|
||||||
>
|
>
|
||||||
<FaSync />
|
<FaSync />
|
||||||
</Button>
|
</Button>
|
||||||
|
<DbMigrateButton hideLabelOnMobile />
|
||||||
{canImport && (
|
{canImport && (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -293,7 +298,6 @@ const WizardFileManager = () => {
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<DbMigrateButton hideLabelOnMobile />
|
|
||||||
{canCreate && (
|
{canCreate && (
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|
@ -328,7 +332,11 @@ const WizardFileManager = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-4">
|
<Loading
|
||||||
|
type="cover"
|
||||||
|
loading={loading}
|
||||||
|
className={classNames('mt-4', loading && 'min-h-[240px]')}
|
||||||
|
>
|
||||||
{filteredFiles.length === 0 && !loading && (
|
{filteredFiles.length === 0 && !loading && (
|
||||||
<div className="text-center py-12">
|
<div className="text-center py-12">
|
||||||
<FaCalendarAlt size={48} className="mx-auto text-gray-400 mb-4" />
|
<FaCalendarAlt size={48} className="mx-auto text-gray-400 mb-4" />
|
||||||
|
|
@ -338,12 +346,6 @@ const WizardFileManager = () => {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{loading && (
|
|
||||||
<p className="text-xs text-gray-400 text-center py-4 animate-pulse">
|
|
||||||
{translate('::App.Platform.LoadingWithThreeDot')}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{viewMode === 'card' ? (
|
{viewMode === 'card' ? (
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3">
|
||||||
{filteredFiles.map((f) => {
|
{filteredFiles.map((f) => {
|
||||||
|
|
@ -511,7 +513,7 @@ const WizardFileManager = () => {
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</Loading>
|
||||||
|
|
||||||
{importFile && (
|
{importFile && (
|
||||||
<WizardImportDialog
|
<WizardImportDialog
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
import type { ReactNode } from 'react'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import { DiffEditor } from '@monaco-editor/react'
|
import { DiffEditor } from '@monaco-editor/react'
|
||||||
import { Button, Dialog, Progress } from '@/components/ui'
|
import { Button, Dialog, Progress } from '@/components/ui'
|
||||||
|
|
@ -23,10 +24,12 @@ import {
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
import { useStoreState } from '@/store/store'
|
import { useStoreState } from '@/store/store'
|
||||||
import Input from '@/components/ui/Input'
|
import Input from '@/components/ui/Input'
|
||||||
|
import { useDialogContext } from '@/components/ui/Dialog/Dialog'
|
||||||
import {
|
import {
|
||||||
WizardImportAnalysisDto,
|
WizardImportAnalysisDto,
|
||||||
WizardImportEntryDto,
|
WizardImportEntryDto,
|
||||||
WizardImportEntryStatusEnum,
|
WizardImportEntryStatusEnum,
|
||||||
|
WizardImportSeedApplyResultDto,
|
||||||
} from '@/proxy/admin/wizard/models'
|
} from '@/proxy/admin/wizard/models'
|
||||||
|
|
||||||
interface WizardImportDialogProps {
|
interface WizardImportDialogProps {
|
||||||
|
|
@ -69,6 +72,29 @@ const formatSize = (size: number) => {
|
||||||
return `${(size / 1024).toFixed(1)} KB`
|
return `${(size / 1024).toFixed(1)} KB`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dialog gövdesi. Normalde yüksekliği viewport'a göre sınırlanır; büyütülmüş (maximize) modda
|
||||||
|
* dialog zaten `100vh` olduğu için gövde esner ve footer en altta, görünür kalır — sabit yükseklik
|
||||||
|
* bırakılırsa footer ekranın dışına taşar. `useDialogContext` yalnızca `Dialog` ağacının içinde
|
||||||
|
* çalıştığından bu ayrı bir bileşendir.
|
||||||
|
*/
|
||||||
|
const ImportDialogBody = ({ children }: { children: ReactNode }) => {
|
||||||
|
const { isMaximized } = useDialogContext()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog.Body
|
||||||
|
className={classNames(
|
||||||
|
'mt-5 flex min-h-0 flex-col gap-3 !overflow-hidden',
|
||||||
|
isMaximized
|
||||||
|
? 'flex-1'
|
||||||
|
: 'h-[calc(100vh-14rem)] max-h-[calc(90vh-7rem)] min-h-[360px] !flex-none',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Dialog.Body>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wizard export zip'ini kapsam klasorune aktarir.
|
* Wizard export zip'ini kapsam klasorune aktarir.
|
||||||
* Akis: analiz → catisma cozumu (diff) → toplu yazma. Yazma sirasinda bir hata olursa
|
* Akis: analiz → catisma cozumu (diff) → toplu yazma. Yazma sirasinda bir hata olursa
|
||||||
|
|
@ -84,6 +110,8 @@ const WizardImportDialog = ({ file, onClose, onImported }: WizardImportDialogPro
|
||||||
const [decisions, setDecisions] = useState<Record<string, EntryDecision>>({})
|
const [decisions, setDecisions] = useState<Record<string, EntryDecision>>({})
|
||||||
const [selectedPath, setSelectedPath] = useState<string | null>(null)
|
const [selectedPath, setSelectedPath] = useState<string | null>(null)
|
||||||
const [applied, setApplied] = useState<AppliedRow[]>([])
|
const [applied, setApplied] = useState<AppliedRow[]>([])
|
||||||
|
// Yazilan data/*.json dosyalarinin veritabanina uygulanma sonucu; oturum kapanisinda doner.
|
||||||
|
const [seedApplied, setSeedApplied] = useState<WizardImportSeedApplyResultDto[]>([])
|
||||||
const [currentPath, setCurrentPath] = useState<string | null>(null)
|
const [currentPath, setCurrentPath] = useState<string | null>(null)
|
||||||
const [showIdentical, setShowIdentical] = useState(false)
|
const [showIdentical, setShowIdentical] = useState(false)
|
||||||
const [editorEpoch, setEditorEpoch] = useState(0)
|
const [editorEpoch, setEditorEpoch] = useState(0)
|
||||||
|
|
@ -228,6 +256,7 @@ const WizardImportDialog = ({ file, onClose, onImported }: WizardImportDialogPro
|
||||||
|
|
||||||
setPhase('running')
|
setPhase('running')
|
||||||
setApplied([])
|
setApplied([])
|
||||||
|
setSeedApplied([])
|
||||||
setError(null)
|
setError(null)
|
||||||
|
|
||||||
const rows: AppliedRow[] = []
|
const rows: AppliedRow[] = []
|
||||||
|
|
@ -267,9 +296,11 @@ const WizardImportDialog = ({ file, onClose, onImported }: WizardImportDialogPro
|
||||||
|
|
||||||
setCurrentPath(null)
|
setCurrentPath(null)
|
||||||
try {
|
try {
|
||||||
await completeWizardImport(analysis.importId)
|
const res = await completeWizardImport(analysis.importId)
|
||||||
} catch {
|
setSeedApplied(res.data.appliedData ?? [])
|
||||||
// Oturum klasoru zaten gecici; temizlenemezse import basarili sayilir.
|
} catch (err: any) {
|
||||||
|
// Dosyalar hedefe yazildi; yalnizca veritabanina uygulama ya da temizlik basarisiz oldu.
|
||||||
|
setError(err?.response?.data?.error?.message ?? err?.message ?? null)
|
||||||
}
|
}
|
||||||
setPhase('finished')
|
setPhase('finished')
|
||||||
onImported()
|
onImported()
|
||||||
|
|
@ -565,7 +596,37 @@ const WizardImportDialog = ({ file, onClose, onImported }: WizardImportDialogPro
|
||||||
<FaCheckCircle />
|
<FaCheckCircle />
|
||||||
{translate('::App.ListForm.WizardImportSuccess')}
|
{translate('::App.ListForm.WizardImportSuccess')}
|
||||||
</div>
|
</div>
|
||||||
|
{seedApplied.length > 0 && (
|
||||||
|
<div className="mt-2">
|
||||||
|
<div className="font-medium">
|
||||||
|
{translate('::App.ListForm.WizardImportDataApplied')}
|
||||||
|
</div>
|
||||||
|
<ul className="mt-0.5 space-y-0.5">
|
||||||
|
{seedApplied.map((row) => (
|
||||||
|
<li key={row.path} className="flex items-center gap-2">
|
||||||
|
<span className="min-w-0 flex-1 truncate font-mono">{row.path}</span>
|
||||||
|
<span className="shrink-0">
|
||||||
|
{row.error ??
|
||||||
|
translate('::App.ListForm.WizardImportDataRowSummary', {
|
||||||
|
inserted: row.insertedRowCount,
|
||||||
|
updated: row.updatedRowCount,
|
||||||
|
skipped: row.skippedRowCount,
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="mt-1">{translate('::App.ListForm.WizardImportRunMigrateHint')}</div>
|
<div className="mt-1">{translate('::App.ListForm.WizardImportRunMigrateHint')}</div>
|
||||||
|
{error && (
|
||||||
|
<div className="mt-2 flex items-start gap-1 text-amber-700 dark:text-amber-300">
|
||||||
|
<FaExclamationTriangle className="mt-0.5 shrink-0" />
|
||||||
|
<span className="break-all">
|
||||||
|
{translate('::App.ListForm.WizardImportDataApplyFailed')} {error}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -581,7 +642,7 @@ const WizardImportDialog = ({ file, onClose, onImported }: WizardImportDialogPro
|
||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
onRequestClose={handleClose}
|
onRequestClose={handleClose}
|
||||||
>
|
>
|
||||||
<Dialog.Body className="mt-5 flex h-[calc(100vh-14rem)] max-h-[calc(90vh-7rem)] min-h-[360px] !flex-none flex-col gap-3 !overflow-hidden">
|
<ImportDialogBody>
|
||||||
<div className="flex flex-wrap items-center gap-2 border-b border-gray-200 pb-3 dark:border-gray-700">
|
<div className="flex flex-wrap items-center gap-2 border-b border-gray-200 pb-3 dark:border-gray-700">
|
||||||
<FaFileImport className="text-sky-500" />
|
<FaFileImport className="text-sky-500" />
|
||||||
<h5 className="mr-1">{translate('::App.ListForm.WizardImportTitle')}</h5>
|
<h5 className="mr-1">{translate('::App.ListForm.WizardImportTitle')}</h5>
|
||||||
|
|
@ -622,7 +683,7 @@ const WizardImportDialog = ({ file, onClose, onImported }: WizardImportDialogPro
|
||||||
|
|
||||||
{(phase === 'running' || phase === 'finished' || phase === 'rolledBack') &&
|
{(phase === 'running' || phase === 'finished' || phase === 'rolledBack') &&
|
||||||
renderProgress()}
|
renderProgress()}
|
||||||
</Dialog.Body>
|
</ImportDialogBody>
|
||||||
|
|
||||||
<Dialog.Footer className="mt-1 flex items-center justify-end gap-2 border-t border-gray-200 pt-3 dark:border-gray-700">
|
<Dialog.Footer className="mt-1 flex items-center justify-end gap-2 border-t border-gray-200 pt-3 dark:border-gray-700">
|
||||||
{phase === 'review' && !error && unresolvedCount > 0 && (
|
{phase === 'review' && !error && unresolvedCount > 0 && (
|
||||||
|
|
|
||||||
|
|
@ -1,297 +0,0 @@
|
||||||
import React, { useState, useEffect } from 'react'
|
|
||||||
import { useComponents } from '../../contexts/ComponentContext'
|
|
||||||
import { FaRegSave, FaSync, FaCog } from 'react-icons/fa'
|
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
|
||||||
import { Formik, Form, Field } from 'formik'
|
|
||||||
import * as Yup from 'yup'
|
|
||||||
import { Button, Checkbox, FormContainer, FormItem, Input } from '@/components/ui'
|
|
||||||
import { usePermission } from '@/utils/hooks/usePermission'
|
|
||||||
import { COMPONENT_PERMISSION } from '@/constants/permission.constant'
|
|
||||||
import Dialog from '@/components/ui/Dialog'
|
|
||||||
import { generateDesignerCode } from '@/components/visualDesigner/codeGenerator'
|
|
||||||
import {
|
|
||||||
createDesignerId,
|
|
||||||
createEmptyDesignerDocument,
|
|
||||||
type DesignerDocument,
|
|
||||||
} from '@/components/visualDesigner/types'
|
|
||||||
|
|
||||||
// Validation schema
|
|
||||||
const validationSchema = Yup.object({
|
|
||||||
name: Yup.string().required(),
|
|
||||||
routePath: Yup.string().required().matches(/^\//, 'Route path must start with /'),
|
|
||||||
description: Yup.string(),
|
|
||||||
dependencies: Yup.array().of(Yup.string()),
|
|
||||||
isActive: Yup.boolean(),
|
|
||||||
})
|
|
||||||
|
|
||||||
interface ComponentEditorProps {
|
|
||||||
componentId?: string | null
|
|
||||||
onClose?: () => void
|
|
||||||
onSaved?: () => void | Promise<void>
|
|
||||||
}
|
|
||||||
|
|
||||||
const ComponentEditor: React.FC<ComponentEditorProps> = ({
|
|
||||||
componentId,
|
|
||||||
onClose,
|
|
||||||
onSaved,
|
|
||||||
}) => {
|
|
||||||
const id = componentId || undefined
|
|
||||||
const { translate } = useLocalization()
|
|
||||||
|
|
||||||
const { getComponent, addComponent, updateComponent, refreshComponent } = useComponents()
|
|
||||||
|
|
||||||
const [isLoaded, setIsLoaded] = useState(false)
|
|
||||||
|
|
||||||
const isEditing = !!id
|
|
||||||
|
|
||||||
// Kaydetme hakki moda gore degisir: mevcut kayit guncelleme, yeni kayit olusturma yetkisi ister.
|
|
||||||
const { checkPermission } = usePermission()
|
|
||||||
const canSave = checkPermission(
|
|
||||||
isEditing ? COMPONENT_PERMISSION.UPDATE : COMPONENT_PERMISSION.CREATE,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Initial values for Formik
|
|
||||||
const [initialValues, setInitialValues] = useState({
|
|
||||||
name: '',
|
|
||||||
routePath: '',
|
|
||||||
description: '',
|
|
||||||
dependencies: [] as string[],
|
|
||||||
isActive: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Load existing component data - sadece edit modunda
|
|
||||||
useEffect(() => {
|
|
||||||
if (isEditing && id && !isLoaded) {
|
|
||||||
let cancelled = false
|
|
||||||
|
|
||||||
void refreshComponent(id).then((freshComponent) => {
|
|
||||||
if (cancelled) return
|
|
||||||
const component = freshComponent
|
|
||||||
if (!component) {
|
|
||||||
setIsLoaded(true)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse dependencies from JSON string
|
|
||||||
let deps: string[] = []
|
|
||||||
try {
|
|
||||||
deps = component.dependencies ? JSON.parse(component.dependencies) : []
|
|
||||||
deps = Array.isArray(deps) ? deps : []
|
|
||||||
} catch {
|
|
||||||
deps = []
|
|
||||||
}
|
|
||||||
|
|
||||||
const values = {
|
|
||||||
name: component.name,
|
|
||||||
routePath: component.routePath,
|
|
||||||
description: component.description || '',
|
|
||||||
dependencies: deps,
|
|
||||||
isActive: component.isActive,
|
|
||||||
}
|
|
||||||
|
|
||||||
setInitialValues(values)
|
|
||||||
setIsLoaded(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
cancelled = true
|
|
||||||
}
|
|
||||||
} else if (!isEditing && !isLoaded) {
|
|
||||||
// Yeni komponent için boş başla - TEMPLATE YOK
|
|
||||||
setIsLoaded(true)
|
|
||||||
}
|
|
||||||
}, [id, isEditing, isLoaded, refreshComponent])
|
|
||||||
|
|
||||||
const createDefaultVisualDocument = (componentName: string): DesignerDocument => {
|
|
||||||
const document = createEmptyDesignerDocument('visual')
|
|
||||||
document.nodes = [
|
|
||||||
{
|
|
||||||
id: createDesignerId(),
|
|
||||||
type: 'span',
|
|
||||||
kind: 'html',
|
|
||||||
props: { children: componentName.trim() },
|
|
||||||
events: {},
|
|
||||||
bindings: {},
|
|
||||||
children: [],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
return document
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSubmit = async (values: typeof initialValues, { setSubmitting }: any) => {
|
|
||||||
if (!values.name.trim()) {
|
|
||||||
alert('Please enter a component name')
|
|
||||||
setSubmitting(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const existingComponent = isEditing && id ? getComponent(id) : undefined
|
|
||||||
const defaultDocument = createDefaultVisualDocument(values.name)
|
|
||||||
const componentData = {
|
|
||||||
name: values.name.trim(),
|
|
||||||
routePath: values.routePath.trim(),
|
|
||||||
description: values.description.trim(),
|
|
||||||
dependencies: JSON.stringify(values.dependencies), // Serialize dependencies to JSON string
|
|
||||||
code: existingComponent
|
|
||||||
? existingComponent.code
|
|
||||||
: generateDesignerCode(values.name, defaultDocument),
|
|
||||||
props: existingComponent
|
|
||||||
? existingComponent.props
|
|
||||||
: JSON.stringify({ visualDesigner: defaultDocument }),
|
|
||||||
isActive: values.isActive,
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isEditing && id) {
|
|
||||||
await updateComponent(id, componentData)
|
|
||||||
} else {
|
|
||||||
await addComponent(componentData)
|
|
||||||
}
|
|
||||||
|
|
||||||
await onSaved?.()
|
|
||||||
onClose?.()
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error saving component:', error)
|
|
||||||
alert('Failed to save component. Please try again.')
|
|
||||||
} finally {
|
|
||||||
setSubmitting(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loading state - komponent yüklenene kadar bekle
|
|
||||||
if (isEditing && !isLoaded) {
|
|
||||||
return (
|
|
||||||
<div className="flex min-h-72 items-center justify-center">
|
|
||||||
<div className="text-center">
|
|
||||||
<FaSync className="w-8 h-8 text-blue-500 animate-spin mx-auto mb-3" />
|
|
||||||
<p className="text-slate-600">{translate('::App.Platform.LoadingWithThreeDot')}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Formik
|
|
||||||
enableReinitialize
|
|
||||||
initialValues={initialValues}
|
|
||||||
validationSchema={validationSchema}
|
|
||||||
onSubmit={handleSubmit}
|
|
||||||
>
|
|
||||||
{({ values, touched, errors, isSubmitting, setFieldValue, submitForm, isValid }) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Form className="pb-3 pt-8">
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div className="bg-white dark:bg-gray-900 rounded-lg shadow-sm border border-slate-200 dark:border-gray-700 p-3">
|
|
||||||
<div className="flex items-center gap-2 mb-3">
|
|
||||||
<div className="bg-blue-100 dark:bg-blue-900/20 p-1.5 rounded-lg">
|
|
||||||
<FaCog className="w-4 h-4 text-blue-600 dark:text-blue-400" />
|
|
||||||
</div>
|
|
||||||
<h2 className="text-base font-semibold text-slate-900 dark:text-gray-100">
|
|
||||||
Component Settings
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<FormContainer size="sm">
|
|
||||||
<FormItem
|
|
||||||
label={translate('::App.DeveloperKitComponentEditor.ComponentName')}
|
|
||||||
invalid={!!(errors.name && touched.name)}
|
|
||||||
errorMessage={errors.name as string}
|
|
||||||
>
|
|
||||||
<Field
|
|
||||||
autoFocus
|
|
||||||
name="name"
|
|
||||||
type="text"
|
|
||||||
component={Input}
|
|
||||||
placeholder="e.g., Button, Card, Modal"
|
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setFieldValue('name', e.target.value)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
|
|
||||||
<FormItem
|
|
||||||
label="Route Path"
|
|
||||||
invalid={!!(errors.routePath && touched.routePath)}
|
|
||||||
errorMessage={errors.routePath as string}
|
|
||||||
>
|
|
||||||
<Field
|
|
||||||
name="routePath"
|
|
||||||
type="text"
|
|
||||||
component={Input}
|
|
||||||
placeholder="e.g., /roles or /admin/reports"
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
|
|
||||||
<FormItem
|
|
||||||
label={translate('::App.Listform.ListformField.Description')}
|
|
||||||
invalid={!!(errors.description && touched.description)}
|
|
||||||
errorMessage={errors.description as string}
|
|
||||||
>
|
|
||||||
<Field
|
|
||||||
name="description"
|
|
||||||
type="text"
|
|
||||||
component={Input}
|
|
||||||
placeholder="Brief description of the component"
|
|
||||||
textArea={true}
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
|
|
||||||
<FormItem
|
|
||||||
label={translate('::App.DeveloperKitComponentEditor.Dependencies')}
|
|
||||||
invalid={!!(errors.dependencies && touched.dependencies)}
|
|
||||||
errorMessage={errors.dependencies as string}
|
|
||||||
>
|
|
||||||
<Field name="dependencies">
|
|
||||||
{() => (
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
value={(values.dependencies || []).join(', ')}
|
|
||||||
placeholder="MyComponent, AnotherComponent, etc."
|
|
||||||
onChange={(e) =>
|
|
||||||
setFieldValue(
|
|
||||||
'dependencies',
|
|
||||||
e.target.value
|
|
||||||
.split(',')
|
|
||||||
.map((s) => s.trim())
|
|
||||||
.filter(Boolean),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
</FormItem>
|
|
||||||
|
|
||||||
<FormItem label={translate('::App.Listform.ListformField.IsActive')}>
|
|
||||||
<Field name="isActive" component={Checkbox} />
|
|
||||||
</FormItem>
|
|
||||||
</FormContainer>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Form>
|
|
||||||
<Dialog.Footer className="flex flex-col items-stretch gap-2 border-t border-slate-200 pt-3 dark:border-slate-700">
|
|
||||||
<div className="flex items-center justify-end gap-2">
|
|
||||||
<Button type="button" size="sm" disabled={isSubmitting} onClick={onClose}>
|
|
||||||
{translate('::App.Platform.Cancel')}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="solid"
|
|
||||||
size="sm"
|
|
||||||
disabled={!canSave || isSubmitting || !values.name.trim() || !isValid}
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
onClick={submitForm}
|
|
||||||
>
|
|
||||||
<FaRegSave className="h-4 w-4" />
|
|
||||||
{isSubmitting ? translate('::App.Platform.Saving') : translate('::App.Platform.Save')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Dialog.Footer>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</Formik>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ComponentEditor
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
import DeveloperLayout from '@/components/layouts/DeveloperLayout'
|
|
||||||
import ComponentManager from '@/views/developerKit/ComponentManager'
|
|
||||||
|
|
||||||
// Existing dynamic-route records can still reference this physical module.
|
|
||||||
// Keep the compatibility entry point, but manage create/view operations in the manager modal.
|
|
||||||
const ComponentEditorPage = () => (
|
|
||||||
<DeveloperLayout>
|
|
||||||
<ComponentManager />
|
|
||||||
</DeveloperLayout>
|
|
||||||
)
|
|
||||||
|
|
||||||
export default ComponentEditorPage
|
|
||||||
|
|
@ -1,505 +0,0 @@
|
||||||
import React, { useEffect, useState } from 'react'
|
|
||||||
import { useComponents } from '../../contexts/ComponentContext'
|
|
||||||
import {
|
|
||||||
FaPlus,
|
|
||||||
FaSearch,
|
|
||||||
FaRegEdit,
|
|
||||||
FaTrashAlt,
|
|
||||||
FaEye,
|
|
||||||
FaEyeSlash,
|
|
||||||
FaFilter,
|
|
||||||
FaCalendarAlt,
|
|
||||||
FaExternalLinkAlt,
|
|
||||||
FaCog,
|
|
||||||
FaSyncAlt,
|
|
||||||
FaTh,
|
|
||||||
FaList,
|
|
||||||
} from 'react-icons/fa'
|
|
||||||
import Widget from '@/components/common/Widget'
|
|
||||||
import { ROUTES_ENUM } from '@/routes/route.constant'
|
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
|
||||||
import { Loading } from '../../components/shared'
|
|
||||||
import PageTitle from '@/components/shared/PageTitle'
|
|
||||||
import { Button } from '@/components/ui'
|
|
||||||
import Dialog from '@/components/ui/Dialog'
|
|
||||||
import ComponentEditor from './ComponentEditor'
|
|
||||||
import { parseComponentDependencies } from '@/contexts/componentRuntime'
|
|
||||||
import { usePermission } from '@/utils/hooks/usePermission'
|
|
||||||
import { COMPONENT_PERMISSION } from '@/constants/permission.constant'
|
|
||||||
import Input from '@/components/ui/Input'
|
|
||||||
import Select from '@/components/ui/Select'
|
|
||||||
import type { CustomComponent } from '@/proxy/developerKit/models'
|
|
||||||
import { useStoreActions, useStoreState } from '@/store/store'
|
|
||||||
import type { ListViewLayoutType } from '../admin/listForm/edit/types'
|
|
||||||
import { ACTION_BUTTON_CLASS, ACTION_BUTTON_HOVER } from '@/constants/actionButton.constant'
|
|
||||||
|
|
||||||
/** Layout tercihi diger listelerle ayni yerde (admin.lists.states) saklanir. */
|
|
||||||
const VIEW_STATE_CODE = 'developerkit-components'
|
|
||||||
|
|
||||||
const ComponentManager: React.FC = () => {
|
|
||||||
const {
|
|
||||||
components,
|
|
||||||
loading,
|
|
||||||
componentErrors,
|
|
||||||
updateComponent,
|
|
||||||
deleteComponent,
|
|
||||||
refreshComponents,
|
|
||||||
} = useComponents()
|
|
||||||
const [searchTerm, setSearchTerm] = useState('')
|
|
||||||
const [filterActive, setFilterActive] = useState<'all' | 'active' | 'inactive'>('all')
|
|
||||||
const [isRefreshing, setIsRefreshing] = useState(false)
|
|
||||||
const [editorComponentId, setEditorComponentId] = useState<string | null | undefined>(undefined)
|
|
||||||
const { states } = useStoreState((state) => state.admin.lists)
|
|
||||||
const { setStates } = useStoreActions((a) => a.admin.lists)
|
|
||||||
const viewMode: ListViewLayoutType =
|
|
||||||
states.find((s) => s.listFormCode === VIEW_STATE_CODE)?.layout === 'grid' ? 'grid' : 'card'
|
|
||||||
const setViewMode = (layout: ListViewLayoutType) =>
|
|
||||||
setStates({ listFormCode: VIEW_STATE_CODE, layout })
|
|
||||||
|
|
||||||
// Calculate statistics
|
|
||||||
const totalComponents = components?.length || 0
|
|
||||||
const activeComponents = components?.filter((c) => c.isActive).length || 0
|
|
||||||
const inactiveComponents = totalComponents - activeComponents
|
|
||||||
const { translate } = useLocalization()
|
|
||||||
|
|
||||||
const filterActiveOptions = [
|
|
||||||
{ value: 'all' as const, label: translate('::App.ComponentFilter.All') },
|
|
||||||
{ value: 'active' as const, label: translate('::App.EntityFilter.Active') },
|
|
||||||
{ value: 'inactive' as const, label: translate('::App.EntityFilter.Inactive') },
|
|
||||||
]
|
|
||||||
const { checkPermission } = usePermission()
|
|
||||||
const canCreate = checkPermission(COMPONENT_PERMISSION.CREATE)
|
|
||||||
const canUpdate = checkPermission(COMPONENT_PERMISSION.UPDATE)
|
|
||||||
const canDelete = checkPermission(COMPONENT_PERMISSION.DELETE)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
void refreshComponents()
|
|
||||||
}, [refreshComponents])
|
|
||||||
|
|
||||||
const handleRefresh = async () => {
|
|
||||||
setIsRefreshing(true)
|
|
||||||
try {
|
|
||||||
await refreshComponents()
|
|
||||||
} finally {
|
|
||||||
setIsRefreshing(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const filteredComponents = components?.filter((component) => {
|
|
||||||
const matchesSearch =
|
|
||||||
component.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
||||||
(component.description || '').toLowerCase().includes(searchTerm.toLowerCase())
|
|
||||||
|
|
||||||
const matchesFilter =
|
|
||||||
filterActive === 'all' ||
|
|
||||||
(filterActive === 'active' && component.isActive) ||
|
|
||||||
(filterActive === 'inactive' && !component.isActive)
|
|
||||||
|
|
||||||
return matchesSearch && matchesFilter
|
|
||||||
})
|
|
||||||
|
|
||||||
const handleToggleActive = async (id: string, isActive: boolean) => {
|
|
||||||
try {
|
|
||||||
const component = components?.find((c) => c.id === id)
|
|
||||||
if (component) {
|
|
||||||
await updateComponent(id, { ...component, isActive })
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to toggle component status:', err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDelete = async (id: string) => {
|
|
||||||
if (window.confirm(translate('::App.DeveloperKitComponent.ConfirmDelete'))) {
|
|
||||||
try {
|
|
||||||
await deleteComponent(id)
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to delete component:', err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderStatusToggle = (component: CustomComponent) => (
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="plain"
|
|
||||||
shape="circle"
|
|
||||||
disabled={!canUpdate}
|
|
||||||
className="!inline-flex !h-auto items-center gap-1 rounded !px-2 py-1 text-xs font-medium transition-colors"
|
|
||||||
onClick={() => handleToggleActive(component.id, !component.isActive)}
|
|
||||||
>
|
|
||||||
{component.isActive ? (
|
|
||||||
<>
|
|
||||||
<FaEye className="w-4 h-4" />
|
|
||||||
{translate('::App.Listform.ListformField.IsActive')}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<FaEyeSlash className="w-4 h-4" />
|
|
||||||
{translate('::App.Platform.Passive')}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
|
|
||||||
const renderActions = (component: CustomComponent) => (
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
{canUpdate && (
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="plain"
|
|
||||||
shape="none"
|
|
||||||
title={translate('::App.Platform.Edit')}
|
|
||||||
icon={<FaRegEdit className="h-4 w-4" />}
|
|
||||||
className={`${ACTION_BUTTON_CLASS} ${ACTION_BUTTON_HOVER.blue}`}
|
|
||||||
onClick={() =>
|
|
||||||
window.open(
|
|
||||||
ROUTES_ENUM.protected.saas.developerKit.componentsEdit.replace(':id', component.id),
|
|
||||||
'_blank',
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="plain"
|
|
||||||
shape="none"
|
|
||||||
disabled={!component.isActive || !component.routePath?.trim()}
|
|
||||||
title={translate('::App.Platform.OpenUrl')}
|
|
||||||
icon={<FaExternalLinkAlt className="h-4 w-4" />}
|
|
||||||
className={`${ACTION_BUTTON_CLASS} ${ACTION_BUTTON_HOVER.slate}`}
|
|
||||||
onClick={() => {
|
|
||||||
const routePath = component.routePath.startsWith('/')
|
|
||||||
? component.routePath
|
|
||||||
: `/${component.routePath}`
|
|
||||||
window.open(routePath, '_blank')
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="plain"
|
|
||||||
shape="none"
|
|
||||||
title={translate('::App.Platform.View')}
|
|
||||||
icon={<FaCog className="h-4 w-4" />}
|
|
||||||
className={`${ACTION_BUTTON_CLASS} ${ACTION_BUTTON_HOVER.green}`}
|
|
||||||
onClick={() => setEditorComponentId(component.id)}
|
|
||||||
/>
|
|
||||||
{canDelete && (
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="plain"
|
|
||||||
shape="none"
|
|
||||||
title={translate('::App.Platform.Delete')}
|
|
||||||
icon={<FaTrashAlt className="h-4 w-4" />}
|
|
||||||
className={`${ACTION_BUTTON_CLASS} ${ACTION_BUTTON_HOVER.red}`}
|
|
||||||
onClick={() => handleDelete(component.id)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<PageTitle title={translate('::' + 'App.DeveloperKit.Components')} />
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mt-2 mb-4">
|
|
||||||
<Widget
|
|
||||||
title={translate('::App.DeveloperKitComponent.Total')}
|
|
||||||
value={totalComponents}
|
|
||||||
color="purple"
|
|
||||||
icon="FaPuzzlePiece"
|
|
||||||
valueClassName="text-3xl"
|
|
||||||
subTitle={translate('::App.DeveloperKitComponent.TotalDescription')}
|
|
||||||
/>
|
|
||||||
<Widget
|
|
||||||
title={translate('::App.DeveloperKitComponent.Active')}
|
|
||||||
value={activeComponents}
|
|
||||||
color="green"
|
|
||||||
icon="FaCheckCircle"
|
|
||||||
valueClassName="text-3xl"
|
|
||||||
subTitle={translate('::App.DeveloperKitComponent.ActiveDescription')}
|
|
||||||
/>
|
|
||||||
<Widget
|
|
||||||
title={translate('::App.DeveloperKitComponent.Inactive')}
|
|
||||||
value={inactiveComponents}
|
|
||||||
color="gray"
|
|
||||||
icon="FaTimesCircle"
|
|
||||||
valueClassName="text-3xl"
|
|
||||||
subTitle={translate('::App.DeveloperKitComponent.InactiveDescription')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Filters */}
|
|
||||||
<div className="flex flex-col sm:flex-row gap-3">
|
|
||||||
<div className="flex-1 relative">
|
|
||||||
<FaSearch className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-slate-400" />
|
|
||||||
<Input
|
|
||||||
unstyle
|
|
||||||
type="text"
|
|
||||||
placeholder={translate('::App.DeveloperKitComponent.SearchPlaceholder')}
|
|
||||||
value={searchTerm}
|
|
||||||
className="w-full pl-10 pr-2 py-1 border border-slate-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<FaFilter className="w-5 h-5 text-slate-500 dark:text-gray-400" />
|
|
||||||
<Select
|
|
||||||
size="sm"
|
|
||||||
className="min-w-[9rem]"
|
|
||||||
options={filterActiveOptions}
|
|
||||||
value={filterActiveOptions.filter((o) => o.value === filterActive)}
|
|
||||||
onChange={(option) => setFilterActive(option?.value ?? 'all')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
disabled={loading || isRefreshing}
|
|
||||||
type="button"
|
|
||||||
variant="default"
|
|
||||||
onClick={() => void handleRefresh()}
|
|
||||||
>
|
|
||||||
<FaSyncAlt className={isRefreshing ? 'animate-spin' : ''} />
|
|
||||||
{translate('::App.Platform.Refresh')}
|
|
||||||
</Button>
|
|
||||||
{canCreate && (
|
|
||||||
<div>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="solid"
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
onClick={() => setEditorComponentId(null)}
|
|
||||||
>
|
|
||||||
<FaPlus className="w-4 h-4" />
|
|
||||||
{translate('::App.DeveloperKitComponent.New')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
icon={<FaTh />}
|
|
||||||
variant={viewMode === 'card' ? 'solid' : 'default'}
|
|
||||||
title={translate('::App.Platform.CardView')}
|
|
||||||
onClick={() => setViewMode('card')}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
icon={<FaList />}
|
|
||||||
variant={viewMode === 'grid' ? 'solid' : 'default'}
|
|
||||||
title={translate('::App.Platform.ListView')}
|
|
||||||
onClick={() => setViewMode('grid')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Components List */}
|
|
||||||
{loading ? (
|
|
||||||
<div className="flex items-center justify-center min-h-screen bg-gray-50">
|
|
||||||
<div className="text-center">
|
|
||||||
<Loading loading={loading} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : filteredComponents?.length > 0 ? (
|
|
||||||
viewMode === 'card' ? (
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-4 gap-4">
|
|
||||||
{filteredComponents.map((component) => (
|
|
||||||
<div
|
|
||||||
key={component.id}
|
|
||||||
className="bg-white dark:bg-gray-900 rounded-lg border border-slate-200 dark:border-gray-700 shadow-sm hover:shadow-md transition-shadow"
|
|
||||||
>
|
|
||||||
<div className="p-4">
|
|
||||||
<div className="flex items-start justify-between">
|
|
||||||
{/* Sol taraf */}
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<div className="flex items-center gap-2 mb-1">
|
|
||||||
<h3
|
|
||||||
className="text-base font-semibold text-slate-900 dark:text-gray-100 truncate"
|
|
||||||
title={component.name}
|
|
||||||
>
|
|
||||||
{component.name}
|
|
||||||
</h3>
|
|
||||||
<div
|
|
||||||
className={`w-2 h-2 shrink-0 rounded-full ${
|
|
||||||
component.isActive ? 'bg-green-500' : 'bg-slate-300 dark:bg-gray-700'
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className="text-slate-600 dark:text-gray-300 text-sm mb-2 truncate">
|
|
||||||
{parseComponentDependencies(component.dependencies).join(', ') ||
|
|
||||||
translate('::App.DeveloperKitComponent.NoDependencies')}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p
|
|
||||||
className="text-slate-600 dark:text-gray-300 text-sm mb-2 truncate"
|
|
||||||
title={component.routePath}
|
|
||||||
>
|
|
||||||
{component.routePath}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{component.description && (
|
|
||||||
<p className="text-slate-600 dark:text-gray-300 text-sm mb-2 line-clamp-2">
|
|
||||||
{component.description}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{componentErrors[component.name] && (
|
|
||||||
<p
|
|
||||||
className="text-xs text-red-700 dark:text-red-300 bg-red-50 dark:bg-red-950 border border-red-200 dark:border-red-800 rounded px-2 py-1 mb-2 break-words"
|
|
||||||
title={componentErrors[component.name]}
|
|
||||||
>
|
|
||||||
{componentErrors[component.name]}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Sag taraf */}
|
|
||||||
{component.lastModificationTime && (
|
|
||||||
<div className="flex items-center gap-1 text-xs text-slate-500 dark:text-gray-400 ml-2 whitespace-nowrap">
|
|
||||||
<FaCalendarAlt className="w-3 h-3" />
|
|
||||||
<span>
|
|
||||||
{new Date(component.lastModificationTime).toLocaleDateString() ?? ''}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Actions */}
|
|
||||||
<div className="flex items-center justify-between gap-1 pt-2 border-t border-slate-100 dark:border-gray-700">
|
|
||||||
{renderStatusToggle(component)}
|
|
||||||
{renderActions(component)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="bg-white dark:bg-gray-900 rounded-lg border border-slate-200 dark:border-gray-700 shadow-sm overflow-x-auto">
|
|
||||||
<table className="w-full text-sm">
|
|
||||||
<thead className="bg-slate-50 dark:bg-gray-800 text-slate-600 dark:text-gray-300">
|
|
||||||
<tr>
|
|
||||||
<th className="text-left font-medium px-4 py-2">
|
|
||||||
{translate('::App.Platform.Name')}
|
|
||||||
</th>
|
|
||||||
<th className="text-left font-medium px-4 py-2">
|
|
||||||
{translate('::App.Platform.Dependencies')}
|
|
||||||
</th>
|
|
||||||
<th className="text-left font-medium px-4 py-2">
|
|
||||||
{translate('::App.Platform.RoutePath')}
|
|
||||||
</th>
|
|
||||||
<th className="text-left font-medium px-4 py-2">
|
|
||||||
{translate('::App.Platform.Description')}
|
|
||||||
</th>
|
|
||||||
<th className="text-left font-medium px-4 py-2 whitespace-nowrap">
|
|
||||||
{translate('::App.Platform.Modified')}
|
|
||||||
</th>
|
|
||||||
<th className="text-left font-medium px-4 py-2">
|
|
||||||
{translate('::App.Platform.Status')}
|
|
||||||
</th>
|
|
||||||
<th className="text-right font-medium px-4 py-2">
|
|
||||||
{translate('::App.Platform.Actions')}
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{filteredComponents.map((component) => (
|
|
||||||
<tr
|
|
||||||
key={component.id}
|
|
||||||
className="border-t border-slate-100 dark:border-gray-700 hover:bg-slate-50 dark:hover:bg-gray-800"
|
|
||||||
>
|
|
||||||
<td className="px-4 py-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div
|
|
||||||
className={`w-2 h-2 shrink-0 rounded-full ${
|
|
||||||
component.isActive ? 'bg-green-500' : 'bg-slate-300 dark:bg-gray-700'
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
<span className="font-medium text-slate-900 dark:text-gray-100">
|
|
||||||
{component.name}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{componentErrors[component.name] && (
|
|
||||||
<p
|
|
||||||
className="mt-1 text-xs text-red-700 dark:text-red-300 break-words"
|
|
||||||
title={componentErrors[component.name]}
|
|
||||||
>
|
|
||||||
{componentErrors[component.name]}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-2 text-slate-600 dark:text-gray-300">
|
|
||||||
{parseComponentDependencies(component.dependencies).join(', ') ||
|
|
||||||
translate('::App.DeveloperKitComponent.NoDependencies')}
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-2 text-slate-600 dark:text-gray-300">
|
|
||||||
{component.routePath}
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-2 text-slate-600 dark:text-gray-300">
|
|
||||||
{component.description}
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-2 text-slate-500 dark:text-gray-400 whitespace-nowrap">
|
|
||||||
{component.lastModificationTime
|
|
||||||
? new Date(component.lastModificationTime).toLocaleDateString()
|
|
||||||
: ''}
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-2">{renderStatusToggle(component)}</td>
|
|
||||||
<td className="px-4 py-2">
|
|
||||||
<div className="flex justify-end">{renderActions(component)}</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<div className="text-center py-12">
|
|
||||||
<div className="mx-auto">
|
|
||||||
<div className="bg-slate-100 rounded-full w-16 h-16 flex items-center justify-center mx-auto mb-4">
|
|
||||||
<FaPlus className="w-8 h-8 text-slate-500" />
|
|
||||||
</div>
|
|
||||||
<h3 className="text-lg font-medium text-slate-900 mb-2">
|
|
||||||
{searchTerm || filterActive !== 'all'
|
|
||||||
? translate('::App.EmptyFiltered.FilteredTitle')
|
|
||||||
: translate('::App.EmptyInitial.InitialTitle')}
|
|
||||||
</h3>
|
|
||||||
<p className="text-slate-600 mb-6">
|
|
||||||
{searchTerm || filterActive !== 'all'
|
|
||||||
? translate('::App.DeveloperKit.EmptyFilteredDescription')
|
|
||||||
: translate('::App.EmptyInitial.InitialDescription')}
|
|
||||||
</p>
|
|
||||||
{canCreate && !searchTerm && filterActive === 'all' && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="inline-flex items-center gap-2 bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors"
|
|
||||||
onClick={() => setEditorComponentId(null)}
|
|
||||||
>
|
|
||||||
<FaPlus className="w-4 h-4" />
|
|
||||||
{translate('::App.ComponentEditorTitle.Create')}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Dialog
|
|
||||||
isOpen={editorComponentId !== undefined}
|
|
||||||
width={720}
|
|
||||||
height="85vh"
|
|
||||||
contentClassName="overflow-y-auto"
|
|
||||||
onClose={() => setEditorComponentId(undefined)}
|
|
||||||
onRequestClose={() => setEditorComponentId(undefined)}
|
|
||||||
>
|
|
||||||
<ComponentEditor
|
|
||||||
key={editorComponentId ?? 'new'}
|
|
||||||
componentId={editorComponentId}
|
|
||||||
onClose={() => setEditorComponentId(undefined)}
|
|
||||||
onSaved={refreshComponents}
|
|
||||||
/>
|
|
||||||
</Dialog>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ComponentManager
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
import ComponentManager from '@/views/developerKit/ComponentManager'
|
|
||||||
import DeveloperLayout from '@/components/layouts/DeveloperLayout'
|
|
||||||
import React from 'react'
|
|
||||||
|
|
||||||
const ComponentManagerPage: React.FC = () => {
|
|
||||||
return (
|
|
||||||
<DeveloperLayout>
|
|
||||||
<ComponentManager />
|
|
||||||
</DeveloperLayout>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ComponentManagerPage
|
|
||||||
|
|
@ -1408,7 +1408,7 @@ const VisualComponentDesigner = () => {
|
||||||
// The tab shows what is being edited: the human readable description when the
|
// The tab shows what is being edited: the human readable description when the
|
||||||
// component carries one, the technical name otherwise.
|
// component carries one, the technical name otherwise.
|
||||||
const pageTitle =
|
const pageTitle =
|
||||||
component?.description?.trim() || component?.name || translate('::App.DeveloperKit.Components')
|
component?.description?.trim() || component?.name || translate('::App.DeveloperKit.CustomComponents')
|
||||||
const customNames = useMemo(
|
const customNames = useMemo(
|
||||||
() => components.filter((item) => item.id !== id && item.isActive).map((item) => item.name),
|
() => components.filter((item) => item.id !== id && item.isActive).map((item) => item.name),
|
||||||
[components, id],
|
[components, id],
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import SurveyModal from './widgets/SurveyModal'
|
||||||
import AnnouncementModal from './widgets/AnnouncementModal'
|
import AnnouncementModal from './widgets/AnnouncementModal'
|
||||||
import EventModal from './widgets/EventModal'
|
import EventModal from './widgets/EventModal'
|
||||||
|
|
||||||
import { Container } from '@/components/shared'
|
import { Container, Loading } from '@/components/shared'
|
||||||
import { usePermission } from '@/utils/hooks/usePermission'
|
import { usePermission } from '@/utils/hooks/usePermission'
|
||||||
import {
|
import {
|
||||||
AnnouncementDto,
|
AnnouncementDto,
|
||||||
|
|
@ -112,9 +112,11 @@ const IntranetDashboard: React.FC = () => {
|
||||||
const [selectedSurvey, setSelectedSurvey] = useState<SurveyDto | null>(null)
|
const [selectedSurvey, setSelectedSurvey] = useState<SurveyDto | null>(null)
|
||||||
const [showSurveyModal, setShowSurveyModal] = useState(false)
|
const [showSurveyModal, setShowSurveyModal] = useState(false)
|
||||||
const [isDesignMode, setIsDesignMode] = useState(false)
|
const [isDesignMode, setIsDesignMode] = useState(false)
|
||||||
const { dashboardLayout: selectedLayout, hiddenWidgetIds, widgetOrder } = useStoreState(
|
const {
|
||||||
(state) => state.admin.dashboard,
|
dashboardLayout: selectedLayout,
|
||||||
)
|
hiddenWidgetIds,
|
||||||
|
widgetOrder,
|
||||||
|
} = useStoreState((state) => state.admin.dashboard)
|
||||||
const {
|
const {
|
||||||
setDashboardLayout,
|
setDashboardLayout,
|
||||||
setHiddenWidgetIds,
|
setHiddenWidgetIds,
|
||||||
|
|
@ -123,6 +125,8 @@ const IntranetDashboard: React.FC = () => {
|
||||||
} = useStoreActions((actions) => actions.admin.dashboard)
|
} = useStoreActions((actions) => actions.admin.dashboard)
|
||||||
|
|
||||||
const [intranetDashboard, setIntranetDashboard] = useState<IntranetDashboardDto>()
|
const [intranetDashboard, setIntranetDashboard] = useState<IntranetDashboardDto>()
|
||||||
|
// Ilk yukleme bitene kadar pano bos gorunuyordu; widget alani yerine spinner gosterilir.
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
const { translate } = useLocalization()
|
const { translate } = useLocalization()
|
||||||
const currentLocale = useLocale()
|
const currentLocale = useLocale()
|
||||||
|
|
||||||
|
|
@ -134,6 +138,8 @@ const IntranetDashboard: React.FC = () => {
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// hata apiService tarafından ele alınıyor
|
// hata apiService tarafından ele alınıyor
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -234,7 +240,9 @@ const IntranetDashboard: React.FC = () => {
|
||||||
const codes = [...new Set(widgetOrder[column] || [])].filter((code) => findWidget(code))
|
const codes = [...new Set(widgetOrder[column] || [])].filter((code) => findWidget(code))
|
||||||
|
|
||||||
widgets
|
widgets
|
||||||
.filter((widget) => widgetColumns[widget.column] === column && !assignedCodes.has(widget.code))
|
.filter(
|
||||||
|
(widget) => widgetColumns[widget.column] === column && !assignedCodes.has(widget.code),
|
||||||
|
)
|
||||||
.forEach((widget) => insertByOrder(codes, widget))
|
.forEach((widget) => insertByOrder(codes, widget))
|
||||||
|
|
||||||
return codes
|
return codes
|
||||||
|
|
@ -642,37 +650,43 @@ const IntranetDashboard: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-12">
|
{loading ? (
|
||||||
{dashboardColumns.map((column, index) => {
|
<Loading loading className="min-h-[320px]" />
|
||||||
const isEmptyDropTarget =
|
) : (
|
||||||
isDesignMode && dragState.targetColumn === column && columnWidgetCodes(column).length === 0
|
<div className="grid grid-cols-1 lg:grid-cols-12">
|
||||||
|
{dashboardColumns.map((column, index) => {
|
||||||
|
const isEmptyDropTarget =
|
||||||
|
isDesignMode &&
|
||||||
|
dragState.targetColumn === column &&
|
||||||
|
columnWidgetCodes(column).length === 0
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={column}
|
key={column}
|
||||||
className={`${columnSpanClasses[activeLayout.columns[index]]} min-h-[100px] space-y-6 rounded-xl p-1 ${
|
className={`${columnSpanClasses[activeLayout.columns[index]]} min-h-[100px] space-y-6 rounded-xl p-1 ${
|
||||||
isDesignMode &&
|
isDesignMode &&
|
||||||
dragState.targetColumn === column &&
|
dragState.targetColumn === column &&
|
||||||
dragState.targetIndex === null
|
dragState.targetIndex === null
|
||||||
? 'bg-blue-50/80 ring-2 ring-blue-300 shadow-lg dark:bg-blue-900/20 dark:ring-blue-600'
|
? 'bg-blue-50/80 ring-2 ring-blue-300 shadow-lg dark:bg-blue-900/20 dark:ring-blue-600'
|
||||||
: 'bg-transparent'
|
: 'bg-transparent'
|
||||||
} transition-all duration-700 ease-out`}
|
} transition-all duration-700 ease-out`}
|
||||||
onDragOver={(e) => handleColumnDragOver(e, column)}
|
onDragOver={(e) => handleColumnDragOver(e, column)}
|
||||||
onDragLeave={handleDragLeaveColumn}
|
onDragLeave={handleDragLeaveColumn}
|
||||||
onDrop={(e) => handleColumnDrop(e, column)}
|
onDrop={(e) => handleColumnDrop(e, column)}
|
||||||
>
|
>
|
||||||
{renderWidgets(column)}
|
{renderWidgets(column)}
|
||||||
{isEmptyDropTarget && (
|
{isEmptyDropTarget && (
|
||||||
<div className="flex h-40 items-center justify-center rounded-xl border-2 border-dashed border-blue-300 bg-blue-50/50 dark:border-blue-600 dark:bg-blue-900/10">
|
<div className="flex h-40 items-center justify-center rounded-xl border-2 border-dashed border-blue-300 bg-blue-50/50 dark:border-blue-600 dark:bg-blue-900/10">
|
||||||
<span className="font-medium text-blue-600 dark:text-blue-400">
|
<span className="font-medium text-blue-600 dark:text-blue-400">
|
||||||
{translate('::App.Dashboard.DropWidgetHere')}
|
{translate('::App.Dashboard.DropWidgetHere')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ import {
|
||||||
} from 'react-icons/fa'
|
} from 'react-icons/fa'
|
||||||
import { useListFormCustomDataSource } from './useListFormCustomDataSource'
|
import { useListFormCustomDataSource } from './useListFormCustomDataSource'
|
||||||
import { useListFormStateStoring } from './useListFormStateStoring'
|
import { useListFormStateStoring } from './useListFormStateStoring'
|
||||||
import { useListFormGridDto } from './shared/hooks'
|
import { useAvailableHeight, useListFormGridDto } from './shared/hooks'
|
||||||
import { Avatar, Dialog, Rate } from '@/components/ui'
|
import { Avatar, Dialog, Rate } from '@/components/ui'
|
||||||
import { getUsers } from '@/services/identity.service'
|
import { getUsers } from '@/services/identity.service'
|
||||||
import { getTenants } from '@/services/tenant.service'
|
import { getTenants } from '@/services/tenant.service'
|
||||||
|
|
@ -236,6 +236,19 @@ const TodoBoardContent = ({
|
||||||
const stateStoring = gridDto.gridOptions.stateStoringDto
|
const stateStoring = gridDto.gridOptions.stateStoringDto
|
||||||
const currentUserName = useStoreState((state) => state.auth.user.userName)
|
const currentUserName = useStoreState((state) => state.auth.user.userName)
|
||||||
const gridRef = useRef<DataGridRef<any, any> | undefined>(undefined)
|
const gridRef = useRef<DataGridRef<any, any> | undefined>(undefined)
|
||||||
|
const boardRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
// Pano sayfanin kalan yuksekligini kaplar, kolonlar kendi icinde kayar. Sabit yukseklik
|
||||||
|
// yalnizca ekran tanimindan gelirse kullanilir; alt form olarak acildiginda pano kendi
|
||||||
|
// icerigi kadar yer kaplar, cunku yuksekligi sahibi kapsayici belirler.
|
||||||
|
const availableHeight = useAvailableHeight(boardRef)
|
||||||
|
const boardHeight =
|
||||||
|
gridDto.gridOptions.height > 0
|
||||||
|
? gridDto.gridOptions.height
|
||||||
|
: isSubForm
|
||||||
|
? undefined
|
||||||
|
: availableHeight
|
||||||
|
|
||||||
const { createSelectDataSource } = useListFormCustomDataSource({ gridRef })
|
const { createSelectDataSource } = useListFormCustomDataSource({ gridRef })
|
||||||
const { getBandedColumns, loadLookupDisplayValues } = useListFormColumns({
|
const { getBandedColumns, loadLookupDisplayValues } = useListFormColumns({
|
||||||
gridDto,
|
gridDto,
|
||||||
|
|
@ -1582,17 +1595,17 @@ const TodoBoardContent = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="pb-3">
|
<div className="flex flex-col pb-3">
|
||||||
{renderToolbar()}
|
{renderToolbar()}
|
||||||
<div className="overflow-x-auto">
|
<div ref={boardRef} className="min-h-0 flex-1 overflow-x-auto" style={{ height: boardHeight }}>
|
||||||
<div className="flex min-h-[420px] min-w-max">
|
<div className="flex h-full min-w-max">
|
||||||
{statuses.map((status) => {
|
{statuses.map((status) => {
|
||||||
const items = itemsForStatus(status)
|
const items = itemsForStatus(status)
|
||||||
const isAdding = newTodoStatus === status
|
const isAdding = newTodoStatus === status
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
key={status}
|
key={status}
|
||||||
className={`group w-[310px] rounded-xl border-2 p-2 transition-all ${draggedColumn !== undefined && columnDropTarget === status ? 'border-violet-400 bg-violet-50 shadow-lg dark:bg-violet-950/30' : draggedKey !== undefined && todoDropTarget?.status === status ? 'border-blue-400 bg-blue-50/70 shadow-lg dark:bg-blue-950/20' : 'border-transparent bg-gray-50 dark:bg-gray-900/60'} ${draggedColumn === status ? 'opacity-50' : ''}`}
|
className={`group flex h-full w-[310px] flex-col rounded-xl border-2 p-2 transition-all ${draggedColumn !== undefined && columnDropTarget === status ? 'border-violet-400 bg-violet-50 shadow-lg dark:bg-violet-950/30' : draggedKey !== undefined && todoDropTarget?.status === status ? 'border-blue-400 bg-blue-50/70 shadow-lg dark:bg-blue-950/20' : 'border-transparent bg-gray-50 dark:bg-gray-900/60'} ${draggedColumn === status ? 'opacity-50' : ''}`}
|
||||||
onDragOver={(event) => {
|
onDragOver={(event) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
if (dragActiveRef.current && draggedColumn) {
|
if (dragActiveRef.current && draggedColumn) {
|
||||||
|
|
@ -1786,7 +1799,7 @@ const TodoBoardContent = ({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="mt-3 space-y-2">
|
<div className="mt-3 min-h-0 flex-1 space-y-2 overflow-y-auto">
|
||||||
{items.map((row, index) => {
|
{items.map((row, index) => {
|
||||||
const key = row[keyField]
|
const key = row[keyField]
|
||||||
const tags = textList(fieldValue(row, options.tagExpr))
|
const tags = textList(fieldValue(row, options.tagExpr))
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,53 @@ export const useWidgetGroupHeight = (
|
||||||
return widgetGroupHeight
|
return widgetGroupHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Elemanın sayfadaki konumuna göre kalan viewport yüksekliğini ölçer.
|
||||||
|
*
|
||||||
|
* Sabit bir çıkarma değeri (`calc(100vh - 170px)`) toolbar sarmalandığında ya da ekran
|
||||||
|
* kırılımı değiştiğinde yanlış sonuç verir; burada elemanın kendi üst konumu ölçüldüğü için
|
||||||
|
* yükseklik her yerleşimde doğru kalır. Eleman henüz bağlanmadıysa `undefined` döner ve
|
||||||
|
* çağıran içerik yüksekliğinde kalır.
|
||||||
|
*
|
||||||
|
* @param minHeight Ölçüm sonucu bunun altına düşmez; dar ekranda liste okunamaz hâle gelmesin diye.
|
||||||
|
* @param bottomGap Alt kenarda bırakılacak boşluk.
|
||||||
|
*/
|
||||||
|
export const useAvailableHeight = (
|
||||||
|
ref: RefObject<HTMLElement | null>,
|
||||||
|
minHeight = 240,
|
||||||
|
bottomGap = 12,
|
||||||
|
) => {
|
||||||
|
const [height, setHeight] = useState<number>()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const element = ref.current
|
||||||
|
if (!element) return
|
||||||
|
|
||||||
|
const measure = () => {
|
||||||
|
const next = Math.max(
|
||||||
|
minHeight,
|
||||||
|
Math.round(window.innerHeight - element.getBoundingClientRect().top - bottomGap),
|
||||||
|
)
|
||||||
|
// Aynı değerde render tetiklenmez; ResizeObserver'ın kendi kendini beslemesi böyle önlenir.
|
||||||
|
setHeight((previous) => (previous === next ? previous : next))
|
||||||
|
}
|
||||||
|
|
||||||
|
measure()
|
||||||
|
|
||||||
|
// Üstteki toolbar/başlık büyüyüp küçüldüğünde eleman aşağı kayar; gövde gözlenerek yakalanır.
|
||||||
|
const resizeObserver = new ResizeObserver(measure)
|
||||||
|
resizeObserver.observe(document.body)
|
||||||
|
window.addEventListener('resize', measure)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
resizeObserver.disconnect()
|
||||||
|
window.removeEventListener('resize', measure)
|
||||||
|
}
|
||||||
|
}, [ref, minHeight, bottomGap])
|
||||||
|
|
||||||
|
return height
|
||||||
|
}
|
||||||
|
|
||||||
/** Liste formuna tanımlı özel JS/CSS kaynaklarını sayfaya ekler. */
|
/** Liste formuna tanımlı özel JS/CSS kaynaklarını sayfaya ekler. */
|
||||||
export const useListFormCustomSources = (gridDto?: GridDto) => {
|
export const useListFormCustomSources = (gridDto?: GridDto) => {
|
||||||
const customJsSources = gridDto?.gridOptions.customJsSources
|
const customJsSources = gridDto?.gridOptions.customJsSources
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue