Dynamic Services için LsitForm tanımlandı

This commit is contained in:
Sedat ÖZTÜRK 2026-09-05 12:06:14 +03:00
parent 76091c0518
commit 54e3390cbf
21 changed files with 12833 additions and 12231 deletions

View file

@ -253,7 +253,9 @@ metnini biçimlendirmek için — sözlükte tipi yoktur, olduğu gibi bileşene
| `SelectTextOnEditStart` | `false` | Düzenlemeye girince metni seç | | `SelectTextOnEditStart` | `false` | Düzenlemeye girince metni seç |
| `EditColumnName` | — | Aksiyon sütununun başlığı | | `EditColumnName` | — | Aksiyon sütununun başlığı |
| `SendOnlyChangedFormValuesUpdate` | `false` | Update'te yalnızca değişen alanları gönder | | `SendOnlyChangedFormValuesUpdate` | `false` | Update'te yalnızca değişen alanları gönder |
| `AddPageUrl` | — | Yeni kayıt popup yerine bu adreste açılır | | `AddPageUrl` | — | Yeni kayıt popup yerine bu adreste açılır (`window.open`; PWA'da aynı sekme) |
| `AddDialogName` | — | Yeni kayıt kayıtlı bir diyalogda açılır (`CommandColumnDto.DialogName` ile aynı liste). `AddPageUrl` ile birlikte verilirse **diyalog önceliklidir**; diyalog kapanınca liste yenilenir |
| `AddDialogParameters` | — | `AddDialogName` diyaloguna geçilen sabit parametreler (JSON nesnesi). Toolbar'da satır bağlamı olmadığı için `@Kolon` token'i kullanılmaz |
| `Popup` | — | Aşağıdaki alt nesne | | `Popup` | — | Aşağıdaki alt nesne |
`Popup` (`GridEditingPopupDto`): `Title`, `ShowTitle` (`true`), `Width` (`700`), `Height` (`500`), `Popup` (`GridEditingPopupDto`): `Title`, `ShowTitle` (`true`), `Width` (`700`), `Height` (`500`),
@ -379,7 +381,7 @@ Ekranın üstündeki KPI şeridi. **Sözleşmesi sezgisel değildir, dikkatle ok
| Anahtar | Ne yazılır | | Anahtar | Ne yazılır |
| --- | --- | | --- | --- |
| `SqlQuery` | Kartları besleyen sorgu. **Zorunludur** — boş olan widget hiç çizilmez | | `SqlQuery` | Kartları besleyen sorgu. **Zorunludur** — boş olan widget hiç çizilmez |
| `Title` · `SubTitle` · `Value` · `Color` · `Icon` · `OnClick` | **Sorgu sonucundaki kolon adları** — sabit metin değil. Sunucu her satırda bu adlı kolonu okur; kolon yoksa boş string döner | | `Title` · `SubTitle` · `Value` · `Color` · `Icon` · `OnClick` | **Sorgu sonucundaki kolon adları** — sabit metin değil. Sunucu her satırda bu adlı kolonu okur; kolon yoksa boş string döner. `Title`/`SubTitle` kolonunun **değeri** ekranda dil anahtarı olarak çözülür (`WidgetGroup`, `::` öneki eklenerek); karşılığı olmayan metin aynen basılır, bu yüzden sorguya dil anahtarı yazılır |
| `ColSpan` · `ColGap` · `ClassName` | Şeritteki yerleşim (widget başına, karta değil) | | `ColSpan` · `ColGap` · `ClassName` | Şeritteki yerleşim (widget başına, karta değil) |
| `ValueClassName` | Değerin CSS sınıfı, varsayılan `text-3xl` | | `ValueClassName` | Değerin CSS sınıfı, varsayılan `text-3xl` |
| `IsActive` | `false` ise sorgu hiç çalıştırılmaz | | `IsActive` | `false` ise sorgu hiç çalıştırılmaz |
@ -1180,7 +1182,7 @@ soneklerini alır.
| **Menus Routes / Module / Menu / Manager** | `App.Menus.Routes` · `.Module` · `.Menu` · `.Manager` (`/admin/menuManager`) | `Sas_H_Route` · `Sas_H_Module` · `Sas_H_Menu` | host | Menü ve route veritabanından gelir; üretilen ekranın menü kaydı buradan açılır. `Menu` ekranı ağaç düzeninin referansıdır, `Routes` yalnızca özel React sayfası eklenirken kullanılır. | | **Menus Routes / Module / Menu / Manager** | `App.Menus.Routes` · `.Module` · `.Menu` · `.Manager` (`/admin/menuManager`) | `Sas_H_Route` · `Sas_H_Module` · `Sas_H_Menu` | host | Menü ve route veritabanından gelir; üretilen ekranın menü kaydı buradan açılır. `Menu` ekranı ağaç düzeninin referansıdır, `Routes` yalnızca özel React sayfası eklenirken kullanılı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/developerkit/dynamic-services` | `Sas_H_DynamicService` | host | Monaco editöründe yazılan, derlenip yayınlanan C# servisi; 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.Components` · `/admin/developerkit/components` | `Sas_H_CustomComponent` | host | Custom component listesi ve **Visual Designer** girişi (§10). |
| **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`. |
@ -1270,7 +1272,6 @@ 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/dynamic-services{,/new,/edit/:id}` | `DynamicServiceManager` · `DynamicServiceEditor` | `App.DeveloperKit.DynamicServices` |
| `/admin/developerkit/components{,/edit/:id}` | `ComponentManagerPage` · `ComponentCodeLayout` | `App.DeveloperKit.Components` | | `/admin/developerkit/components{,/edit/:id}` | `ComponentManagerPage` · `ComponentCodeLayout` | `App.DeveloperKit.Components` |
| `/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` |
@ -1339,7 +1340,7 @@ içinde ilgili `#region` altındadır.
| Onay akışı | `Sales Orders``WorkflowJson` (`ApprovalUserFieldName`, `ApprovalStatusFieldName`, `ApprovalDateFieldName`, `ApprovalDescriptionFieldName`) + `ListFormWorkflow` düğümleri (Start → Approval → End/End) | | Onay akışı | `Sales Orders``WorkflowJson` (`ApprovalUserFieldName`, `ApprovalStatusFieldName`, `ApprovalDateFieldName`, `ApprovalDescriptionFieldName`) + `ListFormWorkflow` düğümleri (Start → Approval → End/End) |
| Alt form (master-detail) | `Tenant → Branches`, `Sales Orders → Sales Order Item`, `Survey → Question → Option` | | Alt form (master-detail) | `Tenant → Branches`, `Sales Orders → Sales Order Item`, `Survey → Question → Option` |
| KPI widget | `User``WidgetsJson`; sorgu `Title`, `Value`, `Color`, `SubTitle`, `Icon` kolonlarını döndürür | | KPI widget | `User``WidgetsJson`; sorgu `Title`, `Value`, `Color`, `SubTitle`, `Icon` kolonlarını döndürür |
| Toolbar'dan diyalog açma | `Tenant` (`TenantsConnectionString`), `Role` (`RolesPermission`), `User` (`UsersPermission`), `Audit Logs` (`AuditLogDetail`), `Notification Rule` (`CreateNotification`), `Branches` (`BranchSeed`), `Sales Orders` (`CreateTenantFromOrderDialog`) | | Toolbar'dan diyalog açma | `Tenant` (`TenantsConnectionString`), `Role` (`RolesPermission`), `User` (`UsersPermission`), `Audit Logs` (`AuditLogDetail`), `Notification Rule` (`CreateNotification`), `Branches` (`BranchSeed`), `Sales Orders` (`CreateTenantFromOrderDialog`), `Dynamic Services` (`DynamicServiceEditor` — hem toolbar hem satır) |
| Yazmayı servise devretme | `Tenant`, `Role`, `User``*ServiceAddress = "list-form-dynamic-api/{tenant\|role\|user}-{insert\|update}"` | | Yazmayı servise devretme | `Tenant`, `Role`, `User``*ServiceAddress = "list-form-dynamic-api/{tenant\|role\|user}-{insert\|update}"` |
| Toplam satırı | `Sales Order Item`, `Licence Item` (`TotalSummaryJson`) | | Toplam satırı | `Sales Order Item`, `Licence Item` (`TotalSummaryJson`) |
| View üzerinden okuma | `Background Worker Jobs`, `Licence Item` (`SelectCommandType = View`) | | View üzerinden okuma | `Background Worker Jobs`, `Licence Item` (`SelectCommandType = View`) |

View file

@ -1264,6 +1264,8 @@ widget, sorgunun döndürdüğü **her satır için bir kart** çizer:
``` ```
- `SqlQuery` **boşsa widget hiç çizilmez**; `IsActive: false` olan da çalıştırılmaz. - `SqlQuery` **boşsa widget hiç çizilmez**; `IsActive: false` olan da çalıştırılmaz.
- `Title`/`SubTitle` kolonunun döndürdüğü **değer dil anahtarıdır**; ekran `::` öneki ekleyip
çevirir, karşılığı yoksa metni aynen basar. Sorguya düz metin değil anahtar yaz.
- Tek sorguyla birden çok kart üretmek için `UNION ALL` kullan — üç durum için üç widget kaydı - Tek sorguyla birden çok kart üretmek için `UNION ALL` kullan — üç durum için üç widget kaydı
açmana gerek yok. açmana gerek yok.
- Widget sorgusu ekranın otomatik tenant/şube filtresine tabi değildir ve `@TENANTID`/`@USERID` - Widget sorgusu ekranın otomatik tenant/şube filtresine tabi değildir ve `@TENANTID`/`@USERID`
@ -1497,12 +1499,16 @@ Kurallar:
- URL token'ları: satır butonunda `@LISTFORMCODE` ve **grid sütunu olarak tanımlı** her alan için - URL token'ları: satır butonunda `@LISTFORMCODE` ve **grid sütunu olarak tanımlı** her alan için
`@FieldName`; toolbar butonunda seçili ilk satırın tüm kolonları. Sütun listesinde olmayan bir `@FieldName`; toolbar butonunda seçili ilk satırın tüm kolonları. Sütun listesinde olmayan bir
alan adı satır butonunda değiştirilmez. alan adı satır butonunda değiştirilmez.
- **Diyalog modu satır butonu içindir.** `@` ile başlayan parametreler satır verisinden okunur; - **Diyalog modunda `@` parametreleri satır butonuna aittir**: değerleri satır verisinden okunur,
toolbar'da satır bağlamı olmadığı için yalnızca sabit parametrelerle çalışır. Satır butonunda toolbar'da satır bağlamı olmadığı için orada yalnızca sabit parametreler çalışır (yeni kayıt
diyalog kapanınca grid otomatik yenilenir. `DialogParameters` boşsa diyalog hiç açılmaz. diyalogu için `{"id":""}` gibi). **`DialogParameters` boşsa diyalog hiç açılmaz** — bu yüzden
parametresiz diyaloglarda da en az bir sabit anahtar yazılır. Her iki konumda da diyalog
kapanınca grid otomatik yenilenir.
- `DialogName` **kayıtlı bir custom component adı değildir**; platformda tanımlı şu diyalogları - `DialogName` **kayıtlı bir custom component adı değildir**; platformda tanımlı şu diyalogları
açar: `RolesPermission`, `UsersPermission`, `TenantsConnectionString`, açar: `RolesPermission`, `UsersPermission`, `TenantsConnectionString`,
`CreateTenantFromOrderDialog`, `CreateNotification`, `AuditLogDetail`, `BranchSeed`. `CreateTenantFromOrderDialog`, `CreateNotification`, `AuditLogDetail`, `BranchSeed`,
`DynamicServiceEditor`. Kayıt yeri `views/shared/DialogContext/DialogShowComponent.tsx`'tir;
her diyalog `open` + `onDialogClose` prop'larını alır.
Kendi ekranını açmak istiyorsan **Adres** modunu kullan (custom component route'u ya da Kendi ekranını açmak istiyorsan **Adres** modunu kullan (custom component route'u ya da
`/admin/list/{ListFormCode}`). `/admin/list/{ListFormCode}`).
- Onay akışının kendi butonları (Onayla/Reddet) `WorkflowJson` üzerinden **otomatik** gelir; - Onay akışının kendi butonları (Onayla/Reddet) `WorkflowJson` üzerinden **otomatik** gelir;
@ -1512,6 +1518,28 @@ Tipik süreç kalıbı: `Url` modunda bir Custom Endpoint'i ya da Dynamic Servic
verilir, dönüşte liste yenilenir. İş mantığı SQL/serviste kalır; ekranda yalnızca buton tanımı verilir, dönüşte liste yenilenir. İş mantığı SQL/serviste kalır; ekranda yalnızca buton tanımı
durur. durur.
**Kalıp — yazma tarafı ayrı bir sayfada olan liste.** Kayıt grid'in popup formuyla değil, var
olan bir React sayfasıyla (Monaco editörü, sihirbaz, tasarımcı) düzenleniyorsa liste yine bir
ListForm'dur; üç aksiyon şöyle bağlanır:
| Aksiyon | Nereye yazılır |
| --- | --- |
| Ekle | `EditingOptionJson`: `AllowAdding: true` + `AddDialogName` (+ sabit `AddDialogParameters`) — toolbar'ın **hazır Add butonu** grid popup'i yerine diyaloğu açar. Ayrı sayfaya gidilecekse `AddDialogName` yerine `AddPageUrl`. Her iki durumda da ayrı bir toolbar butonu **tanımlama**: hazır buton doğru konumda (ilk aksiyon) ve doğru görünümdedir, yetkisi `PermissionJson.C`'den gelir |
| Düzenle | Satır butonu (`ButtonPosition: 0`), `DialogName` + `{"id":"@Id"}`; ayrı sayfa yolunda `Url` modu (`/admin/list/{Kod}/edit/@Id`) |
| Sil | Kayıt yalnızca tablodan silinmiyorsa (assembly/dosya/dış kayıt da düşecekse) satır butonu + `OnClick` ile `UiEvalService` aksiyonu; düz tabloda `AllowDeleting` + `DeleteCommand` yeterlidir |
`AllowUpdating`/`AllowDeleting` `false` bırakılır, böylece grid içinde ikinci bir düzenleme yolu
ılmaz; `AllowAdding` yalnızca hazır Add butonunun çizilmesi için `true`'dur ve `AddDialogName`
olduğu sürece popup form açmaz. Diyalog kapanışı listeyi yenilediği için kaydettikten sonra ayrıca bir
yenileme çağrısı gerekmez. Canlı örnek: **Dynamic Services** (`App.DeveloperKit.DynamicServices`) —
liste, KPI şeridi ve Swagger butonu ListForm'da; kod yazma `DynamicServiceEditor` diyalogunda (§9.3).
> **Adres değişiminde veritabanı uyarısı:** `MenusData.json`'daki `Menus` ve `Routes` kayıtları
> **yalnızca yoksa** eklenir (`MenuDataSeeder`). Var olan bir menünün `Url`'ini ya da bir route'un
> `Path`'ini değiştirdiğinde kurulu veritabanları eski adreste kalır; düzeltme
> `{sql|postgres}/execute/` altında bir prosedürle (§8.3) ya da elle yapılır. Yeni kurulumda
> sorun çıkmadığı için bu adım kolayca atlanır.
### 4.10 Wizard adım haritası — ekrandaki her adımın seed karşılığı ### 4.10 Wizard adım haritası — ekrandaki her adımın seed karşılığı
`/admin/listform/wizard` sihirbazı (kaynak: `wizard/Wizard.tsx` + `WizardStep*.tsx`) aşağıdaki `/admin/listform/wizard` sihirbazı (kaynak: `wizard/Wizard.tsx` + `WizardStep*.tsx`) aşağıdaki
@ -2858,7 +2886,8 @@ source'ları için de tercih edilen hedef budur.
### 9.3 Dynamic Service — çalışma zamanında derlenen C# servisi ### 9.3 Dynamic Service — çalışma zamanında derlenen C# servisi
Ekran: `/admin/developerkit/dynamic-services` (Monaco editörü). Entity: `DynamicService` Ekran: liste `/admin/list/App.DeveloperKit.DynamicServices` (ListForm); kod editörü aynı listenin
toolbar/satır butonlarından açılan `DynamicServiceEditor` diyalogu (Monaco). Entity: `DynamicService`
(`FullAuditedEntity` + `IMultiTenant`). Karar sırasının **3. adımıdır**: konfigürasyon ve SQL (`FullAuditedEntity` + `IMultiTenant`). Karar sırasının **3. adımıdır**: konfigürasyon ve SQL
yetmediğinde gelir, çekirdek kod değişikliğinden önce denenir. yetmediğinde gelir, çekirdek kod değişikliğinden önce denenir.
@ -2999,7 +3028,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/developerkit/dynamic-services` | `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/developerkit/components` | `App.DeveloperKit.Components` | 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 |
@ -3358,6 +3387,7 @@ platformun hazır aksiyonları buradan çağrılır. Uydurma metot yazma; kullan
| `UiEvalService.ApiGenerateBackgroundWorkers()` | Zamanlanmış iş tanımlarını yeniden üretir | — | | `UiEvalService.ApiGenerateBackgroundWorkers()` | Zamanlanmış iş tanımlarını yeniden üretir | — |
| `UiEvalService.ApiClearRedisCache()` | Dağıtık önbelleği temizler | — | | `UiEvalService.ApiClearRedisCache()` | Dağıtık önbelleği temizler | — |
| `UiEvalService.ApiKickUser(userId, gridRef)` | Kullanıcının oturumunu düşürür, grid'i yeniler | — | | `UiEvalService.ApiKickUser(userId, gridRef)` | Kullanıcının oturumunu düşürür, grid'i yeniler | — |
| `UiEvalService.ApiDeleteDynamicService(id, gridRef)` | Dynamic Service kaydını onay alarak siler (yüklü assembly de düşer), grid'i yeniler | `App.DeveloperKit.DynamicServices.Delete` |
| `UiEvalService.ApiDbMigrate()` | Migration + seed akışını başlatır, log panelini açar | `App.Setup.Migrate` | | `UiEvalService.ApiDbMigrate()` | Migration + seed akışını başlatır, log panelini açar | `App.Setup.Migrate` |
### 9.14 Platformun hazır gelen diğer konfigürasyon ekranları ### 9.14 Platformun hazır gelen diğer konfigürasyon ekranları

View file

@ -605,7 +605,7 @@ Teknik kullanıcılar için `/admin/developerkit` altında toplanan araçlar:
| **SQL Query Manager** | `/admin/sqlQueryManager` | Nesne gezgini, Monaco tabanlı SQL editörü, sonuç grid'i, **tablo tasarımcısı**, **view tasarımcısı** ve **CRUD endpoint yönetimi**. | | **SQL Query Manager** | `/admin/sqlQueryManager` | Nesne gezgini, Monaco tabanlı SQL editörü, sonuç grid'i, **tablo tasarımcısı**, **view tasarımcısı** ve **CRUD endpoint yönetimi**. |
| **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/developerkit/dynamic-services` | C# kodunu tarayıcıda yazıp 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/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}`. |
| **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. |

View file

@ -44,5 +44,18 @@ public class GridEditingDto
/// Popup olarak açılmadan yeni kayıt ekleme işlemi yapılacaksa, yeni kaydın ekleneceği sayfanın url'si /// Popup olarak açılmadan yeni kayıt ekleme işlemi yapılacaksa, yeni kaydın ekleneceği sayfanın url'si
/// </summary> /// </summary>
public string AddPageUrl { get; set; } public string AddPageUrl { get; set; }
/// <summary>
/// Yeni kayıt grid popup'i yerine kayıtlı bir diyalogda açılacaksa diyalogun adı
/// (<c>CommandColumnDto.DialogName</c> ile aynı liste). <see cref="AddPageUrl"/> ile birlikte
/// verilirse diyalog önceliklidir.
/// </summary>
public string AddDialogName { get; set; }
/// <summary>
/// <see cref="AddDialogName"/> diyaloguna geçilecek sabit parametreler (JSON nesnesi).
/// Toolbar'da satır bağlamı olmadığı için <c>@Kolon</c> token'i kullanılmaz.
/// </summary>
public string AddDialogParameters { get; set; }
} }

View file

@ -4986,6 +4986,18 @@
"en": "Add Page Url", "en": "Add Page Url",
"tr": "Ek Sayfa Url'si" "tr": "Ek Sayfa Url'si"
}, },
{
"resourceName": "Platform",
"key": "App.ListFormEdit.EditingAddDialogName",
"en": "Add Dialog Name",
"tr": "Ekleme Diyalog Adı"
},
{
"resourceName": "Platform",
"key": "App.ListFormEdit.EditingAddDialogParameters",
"en": "Add Dialog Parameters",
"tr": "Ekleme Diyalog Parametreleri"
},
{ {
"resourceName": "Platform", "resourceName": "Platform",
"key": "App.ListFormEdit.EditingAllowDetail", "key": "App.ListFormEdit.EditingAllowDetail",
@ -17919,9 +17931,57 @@
{ {
"resourceName": "Platform", "resourceName": "Platform",
"key": "App.DeveloperKitDynamicServices.TotalDescription", "key": "App.DeveloperKitDynamicServices.TotalDescription",
"en": "Number of defined services",
"tr": "Tanımlı servislerin sayısı"
},
{
"resourceName": "Platform",
"key": "App.DeveloperKitDynamicServices.Total",
"en": "Total",
"tr": "Toplam"
},
{
"resourceName": "Platform",
"key": "App.DeveloperKitDynamicServices.FailedDescription",
"en": "Number of failed services", "en": "Number of failed services",
"tr": "Başarısız servislerin sayısı" "tr": "Başarısız servislerin sayısı"
}, },
{
"resourceName": "Platform",
"key": "App.DeveloperKit.DynamicServices.Description",
"en": "Dynamic services compiled and published at runtime",
"tr": "Çalışma zamanında derlenip yayınlanan dinamik servisler"
},
{
"resourceName": "Platform",
"key": "App.Listform.ListformField.CompilationStatus",
"en": "Compilation Status",
"tr": "Derleme Durumu"
},
{
"resourceName": "Platform",
"key": "App.Listform.ListformField.ControllerName",
"en": "Controller Name",
"tr": "Controller Adı"
},
{
"resourceName": "Platform",
"key": "App.Listform.ListformField.PrimaryEntityType",
"en": "Primary Entity Type",
"tr": "Ana Entity Türü"
},
{
"resourceName": "Platform",
"key": "App.Listform.ListformField.LastCompilationError",
"en": "Last Compilation Error",
"tr": "Son Derleme Hatası"
},
{
"resourceName": "Platform",
"key": "App.Listform.ListformField.LastSuccessfulCompilation",
"en": "Last Successful Compilation",
"tr": "Son Başarılı Derleme"
},
{ {
"resourceName": "Platform", "resourceName": "Platform",
"key": "App.DeveloperKitDynamicServices.ActiveDescription", "key": "App.DeveloperKitDynamicServices.ActiveDescription",
@ -17934,12 +17994,6 @@
"en": "Number of passive services", "en": "Number of passive services",
"tr": "Pasif servislerin sayısı" "tr": "Pasif servislerin sayısı"
}, },
{
"resourceName": "Platform",
"key": "App.DeveloperKitDynamicServices.SearchPlaceholder",
"en": "Search service...",
"tr": "Servis ara..."
},
{ {
"resourceName": "Platform", "resourceName": "Platform",
"key": "App.DeveloperKitDynamicServices.FilterPending", "key": "App.DeveloperKitDynamicServices.FilterPending",
@ -17952,30 +18006,6 @@
"en": "New Service", "en": "New Service",
"tr": "Yeni Servis" "tr": "Yeni Servis"
}, },
{
"resourceName": "Platform",
"key": "App.DeveloperKitDynamicServices.NoServicesYet",
"en": "No services yet",
"tr": "Henüz servis yok"
},
{
"resourceName": "Platform",
"key": "App.DeveloperKitDynamicServices.TryChangingFilter",
"en": "Try changing filter or search criteria.",
"tr": "Filtre veya arama kriterlerini değiştirmeyi deneyin."
},
{
"resourceName": "Platform",
"key": "App.DeveloperKitDynamicServices.GetStarted",
"en": "Start by creating your first dynamic service.",
"tr": "İlk dinamik servisinizi oluşturmak için başlayın."
},
{
"resourceName": "Platform",
"key": "App.DeveloperKitDynamicServices.CreateNewService",
"en": "Create New Service",
"tr": "Yeni Servis Oluştur"
},
{ {
"resourceName": "Platform", "resourceName": "Platform",
"key": "App.DeveloperKitDynamicServices.DeleteConfirm", "key": "App.DeveloperKitDynamicServices.DeleteConfirm",

View file

@ -328,30 +328,6 @@
"routeType": "protected", "routeType": "protected",
"authority": ["App.SqlQueryManager"] "authority": ["App.SqlQueryManager"]
}, },
{
"key": "admin.developerkit.dynamic-services",
"path": "/admin/developerkit/dynamic-services",
"componentType": "normal",
"componentPath": "@/views/developerKit/DynamicServiceManager",
"routeType": "protected",
"authority": ["App.DeveloperKit.DynamicServices"]
},
{
"key": "admin.developerkit.dynamic-services.new",
"path": "/admin/developerkit/dynamic-services/new",
"componentType": "normal",
"componentPath": "@/views/developerKit/DynamicServiceEditor",
"routeType": "protected",
"authority": ["App.DeveloperKit.DynamicServices"]
},
{
"key": "admin.developerkit.dynamic-services.edit",
"path": "/admin/developerkit/dynamic-services/edit/:id",
"componentType": "normal",
"componentPath": "@/views/developerKit/DynamicServiceEditor",
"routeType": "protected",
"authority": ["App.DeveloperKit.DynamicServices"]
},
{ {
"key": "admin.developerkit.components", "key": "admin.developerkit.components",
"path": "/admin/developerkit/components", "path": "/admin/developerkit/components",
@ -1013,7 +989,7 @@
"Code": "App.DeveloperKit.DynamicServices", "Code": "App.DeveloperKit.DynamicServices",
"DisplayName": "App.DeveloperKit.DynamicServices", "DisplayName": "App.DeveloperKit.DynamicServices",
"Order": 4, "Order": 4,
"Url": "/admin/developerkit/dynamic-services", "Url": "/admin/list/App.DeveloperKit.DynamicServices",
"Icon": "FcCommandLine", "Icon": "FcCommandLine",
"RequiredPermissionName": "App.DeveloperKit.DynamicServices", "RequiredPermissionName": "App.DeveloperKit.DynamicServices",
"IsDisabled": false, "IsDisabled": false,

View file

@ -474,6 +474,8 @@ public static class SeedConsts
public const string CustomEndpoints = Default + ".CustomEndpoints"; public const string CustomEndpoints = Default + ".CustomEndpoints";
public const string IntranetWidgets = Default + ".IntranetWidgets"; public const string IntranetWidgets = Default + ".IntranetWidgets";
public const string DynamicServices = Default + ".DynamicServices";
} }
public const string Forum = Prefix.App + ".Forum"; public const string Forum = Prefix.App + ".Forum";

View file

@ -46,7 +46,7 @@ function DbMigrateLogPanel({ onClose }: DbMigrateLogPanelProps) {
return ( return (
<div className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/60"> <div className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/60">
<div className="flex flex-col w-[700px] max-w-[95vw] h-[520px] max-h-[90vh] rounded-xl shadow-2xl bg-gray-900 border border-gray-700"> <div className="flex flex-col w-[800px] max-w-[95vw] h-[520px] max-h-[90vh] rounded-xl shadow-2xl bg-gray-900 border border-gray-700">
<div className="flex items-center justify-between px-4 py-3 border-b border-gray-700"> <div className="flex items-center justify-between px-4 py-3 border-b border-gray-700">
<span className="text-white font-semibold text-sm">DB Migration Logs</span> <span className="text-white font-semibold text-sm">DB Migration Logs</span>
{/* {/*

View file

@ -2,9 +2,15 @@ import classNames from 'classnames'
import Widget, { type colorType } from './Widget' import Widget, { type colorType } from './Widget'
import { WidgetEditDto, WidgetGroupDto } from '@/proxy/form/models' import { WidgetEditDto, WidgetGroupDto } from '@/proxy/form/models'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { useLocalization } from '@/utils/hooks/useLocalization'
export default function WidgetGroup({ widgetGroups }: { widgetGroups: WidgetGroupDto[] }) { export default function WidgetGroup({ widgetGroups }: { widgetGroups: WidgetGroupDto[] }) {
const navigate = useNavigate() const navigate = useNavigate()
const { translate } = useLocalization()
// Sorgudan gelen basliklar dil anahtaridir; karsiligi yoksa translate metni aynen dondurur,
// boylece duz metin donduren eski widget sorgulari da calismaya devam eder.
const asText = (value?: string) => (value ? translate('::' + value) : value)
return ( return (
<div> <div>
@ -28,11 +34,11 @@ export default function WidgetGroup({ widgetGroups }: { widgetGroups: WidgetGrou
style={{ gridColumn: `span ${group.colSpan} / span ${group.colSpan}` }} style={{ gridColumn: `span ${group.colSpan} / span ${group.colSpan}` }}
> >
<Widget <Widget
title={item.title} title={asText(item.title) as string}
value={item.value} value={item.value}
color={item.color as colorType} color={item.color as colorType}
icon={item.icon} icon={item.icon}
subTitle={item.subTitle} subTitle={asText(item.subTitle)}
valueClassName={item.valueClassName} valueClassName={item.valueClassName}
onClick={() => { onClick={() => {
if (item.onClick) { if (item.onClick) {

View file

@ -497,6 +497,8 @@ export interface GridEditingDto {
isBranch: boolean isBranch: boolean
isOrganizationUnit: boolean isOrganizationUnit: boolean
addPageUrl?: string addPageUrl?: string
addDialogName?: string
addDialogParameters?: string
} }
export interface EditingFormDto { export interface EditingFormDto {

View file

@ -41,9 +41,6 @@
entitiesEdit: '/admin/developerkit/entities/edit/:id', entitiesEdit: '/admin/developerkit/entities/edit/:id',
components: '/admin/developerkit/components', components: '/admin/developerkit/components',
componentsEdit: '/admin/developerkit/components/edit/:id', componentsEdit: '/admin/developerkit/components/edit/:id',
dynamicServices: '/admin/developerkit/dynamic-services',
dynamicServicesNew: '/admin/developerkit/dynamic-services/new',
dynamicServicesEdit: '/admin/developerkit/dynamic-services/edit/:id',
}, },
reports: { reports: {
view: '/admin/reports/:report/view/:id?/:listFormCode?', view: '/admin/reports/:report/view/:id?/:listFormCode?',

View file

@ -4,6 +4,7 @@ import { getLocalization } from '@/services/localization.service'
import { store } from '@/store' import { store } from '@/store'
import { clearRedisCache } from './languageText.service' import { clearRedisCache } from './languageText.service'
import { kickUser } from './identity.service' import { kickUser } from './identity.service'
import { dynamicServiceService } from './dynamicService.service'
import { streamSetupMigrate } from './setup.service' import { streamSetupMigrate } from './setup.service'
import { openDbMigrateLogPanel, dispatchMigrateLog, dispatchMigrateDone } from '@/components/shared/DbMigrateLogPanel' import { openDbMigrateLogPanel, dispatchMigrateLog, dispatchMigrateDone } from '@/components/shared/DbMigrateLogPanel'
@ -54,6 +55,39 @@ export abstract class UiEvalService {
}, '::App.KickUser.KickUserMessage') }, '::App.KickUser.KickUserMessage')
} }
/**
* Dynamic Service kaydını siler. Ham SQL yerine servisten geçer; yalnızca böyle
* yüklenmiş assembly ve Swagger kaydı da runtime'dan düşer.
*/
static ApiDeleteDynamicService = (id: string, gridRef?: any) => {
if (!id) return
if (!window.confirm(UiEvalService.translate('::App.DeveloperKitDynamicServices.DeleteConfirm'))) {
return
}
void dynamicServiceService
.delete(id)
.then(() => {
if (gridRef?.current) {
gridRef.current.instance().refresh()
}
toast.push(
<Notification type="success" duration={2000}>
{UiEvalService.translate('::App.Platform.ObjectDeletedSuccessfully')}
</Notification>,
{ placement: 'bottom-end' },
)
})
.catch(() => {
toast.push(
<Notification type="danger" duration={5000}>
{UiEvalService.translate('::App.DeveloperKitDynamicServices.DeleteError')}
</Notification>,
{ placement: 'bottom-end' },
)
})
}
/** /**
* Canlı sistemde migration tetikler (SQL Query Manager ve Wizard dosya yöneticisi ortak kullanır). * Canlı sistemde migration tetikler (SQL Query Manager ve Wizard dosya yöneticisi ortak kullanır).
* Yetki sunucu tarafında `App.Setup.Migrate` izniyle doğrulanır. * Yetki sunucu tarafında `App.Setup.Migrate` izniyle doğrulanır.

View file

@ -57,25 +57,9 @@ export interface PublishDto {
isActive?: boolean isActive?: boolean
} }
export interface DynamicAppServiceListResult {
items: DynamicServiceDto[]
totalCount: number
}
class DynamicServiceService { class DynamicServiceService {
private readonly baseUrl = '/api/app/dynamic-app-service' private readonly baseUrl = '/api/app/dynamic-app-service'
/**
* Tüm dynamic app service'leri listele
*/
async getList(): Promise<DynamicAppServiceListResult> {
const response = await apiService.fetchData<DynamicAppServiceListResult>({
url: this.baseUrl,
method: 'GET',
})
return response.data
}
/** /**
* ID'ye göre dynamic app service detayını getir * ID'ye göre dynamic app service detayını getir
*/ */
@ -87,30 +71,6 @@ class DynamicServiceService {
return response.data return response.data
} }
/**
* Yeni dynamic app service oluştur
*/
async create(data: DynamicServiceDto): Promise<DynamicServiceDto> {
const response = await apiService.fetchData<DynamicServiceDto>({
url: this.baseUrl,
method: 'POST',
data: data as any,
})
return response.data
}
/**
* Dynamic app service güncelle
*/
async update(id: string, data: Partial<DynamicServiceDto>): Promise<DynamicServiceDto> {
const response = await apiService.fetchData<DynamicServiceDto>({
url: `${this.baseUrl}/${id}`,
method: 'PUT',
data: data as any,
})
return response.data
}
/** /**
* Dynamic app service sil * Dynamic app service sil
*/ */
@ -144,28 +104,6 @@ class DynamicServiceService {
}) })
return response.data return response.data
} }
/**
* Dynamic app service'i aktif/pasif yap
*/
async toggleActive(id: string, isActive: boolean): Promise<void> {
await apiService.fetchData<void>({
url: `${this.baseUrl}/${id}/toggle-active`,
method: 'POST',
data: { isActive } as any,
})
}
/**
* Compilation durumunu getir
*/
async getCompilationStatus(id: string): Promise<{ status: string; error?: string }> {
const response = await apiService.fetchData<{ status: string; error?: string }>({
url: `${this.baseUrl}/${id}/compilation-status`,
method: 'GET',
})
return response.data
}
} }
export const dynamicServiceService = new DynamicServiceService() export const dynamicServiceService = new DynamicServiceService()

View file

@ -75,6 +75,38 @@ function FormTabEdit(props: FormEditProps & { listFormCode: string }) {
component={Input} component={Input}
/> />
</FormItem> </FormItem>
<FormItem
label={translate('::App.ListFormEdit.EditingAddDialogName')}
invalid={
errors.editingOptionDto?.addDialogName &&
touched.editingOptionDto?.addDialogName
}
errorMessage={errors.editingOptionDto?.addDialogName}
>
<Field
type="text"
autoComplete="off"
name="editingOptionDto.addDialogName"
placeholder={translate('::App.ListFormEdit.EditingAddDialogName')}
component={Input}
/>
</FormItem>
<FormItem
label={translate('::App.ListFormEdit.EditingAddDialogParameters')}
invalid={
errors.editingOptionDto?.addDialogParameters &&
touched.editingOptionDto?.addDialogParameters
}
errorMessage={errors.editingOptionDto?.addDialogParameters}
>
<Field
type="text"
autoComplete="off"
name="editingOptionDto.addDialogParameters"
placeholder={translate('::App.ListFormEdit.EditingAddDialogParameters')}
component={Input}
/>
</FormItem>
<FormItem <FormItem
label={translate('::App.ListFormEdit.AllowUpdating')} label={translate('::App.ListFormEdit.AllowUpdating')}
invalid={ invalid={
@ -135,6 +167,23 @@ function FormTabEdit(props: FormEditProps & { listFormCode: string }) {
component={Checkbox} component={Checkbox}
/> />
</FormItem> </FormItem>
</div>
<div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<FormItem
label={translate('::App.ListFormEdit.EditingConfirmDelete')}
invalid={
errors.editingOptionDto?.confirmDelete &&
touched.editingOptionDto?.confirmDelete
}
errorMessage={errors.editingOptionDto?.confirmDelete}
>
<Field
name="editingOptionDto.confirmDelete"
placeholder={translate('::App.ListFormEdit.EditingConfirmDelete')}
component={Checkbox}
/>
</FormItem>
<FormItem <FormItem
label={translate('::App.ListFormEdit.EditingAllowDeleting')} label={translate('::App.ListFormEdit.EditingAllowDeleting')}
invalid={ invalid={
@ -163,23 +212,8 @@ function FormTabEdit(props: FormEditProps & { listFormCode: string }) {
component={Checkbox} component={Checkbox}
/> />
</FormItem> </FormItem>
</div> </div>
<div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3"> <div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<FormItem
label={translate('::App.ListFormEdit.EditingConfirmDelete')}
invalid={
errors.editingOptionDto?.confirmDelete &&
touched.editingOptionDto?.confirmDelete
}
errorMessage={errors.editingOptionDto?.confirmDelete}
>
<Field
name="editingOptionDto.confirmDelete"
placeholder={translate('::App.ListFormEdit.EditingConfirmDelete')}
component={Checkbox}
/>
</FormItem>
<FormItem <FormItem
label={translate('::App.ListFormEdit.EditingUseIcons')} label={translate('::App.ListFormEdit.EditingUseIcons')}
invalid={ invalid={

View file

@ -27,7 +27,6 @@ import {
} from '../../../../proxy/admin/list-form-field/models' } from '../../../../proxy/admin/list-form-field/models'
import { enumToList } from '../../../../utils/enumUtils' import { enumToList } from '../../../../utils/enumUtils'
import { PREFIX } from '@/constants/theme.constant' import { PREFIX } from '@/constants/theme.constant'
import { iconList } from '@/utils/iconList'
export enum WidgetColorEnum { export enum WidgetColorEnum {
'Blue' = 'blue', 'Blue' = 'blue',
@ -547,10 +546,6 @@ export const pivotSettingsAreaOptions = enumToList<string>(PivotSettingsAreaEnum
export const pivotSettingsGroupIntervalOptions = enumToList<string>(PivotSettingsGroupIntervalEnum) export const pivotSettingsGroupIntervalOptions = enumToList<string>(PivotSettingsGroupIntervalEnum)
export const pivotSortDirectionOptions = enumToList<string>(ColumnSortDirectionEnum) export const pivotSortDirectionOptions = enumToList<string>(ColumnSortDirectionEnum)
export const widgetColorOptions = enumToList<string>(WidgetColorEnum) export const widgetColorOptions = enumToList<string>(WidgetColorEnum)
export const widgetIconOptions = iconList.map((icon) => ({
value: icon,
label: icon,
}))
// Tema renk haritası (border ve fill) // Tema renk haritası (border ve fill)
const styleColorMap: Record< const styleColorMap: Record<
string, string,

View file

@ -1,5 +1,4 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react' import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { useParams, useNavigate, Link } from 'react-router-dom'
import { Editor } from '@monaco-editor/react' import { Editor } from '@monaco-editor/react'
import type { editor } from 'monaco-editor' import type { editor } from 'monaco-editor'
import { import {
@ -8,7 +7,6 @@ import {
FaCheckCircle, FaCheckCircle,
FaExclamationCircle, FaExclamationCircle,
FaSpinner, FaSpinner,
FaArrowLeft,
FaCog, FaCog,
FaCode, FaCode,
FaSave, FaSave,
@ -19,9 +17,9 @@ import {
type CompileResult, type CompileResult,
type PublishResult, type PublishResult,
} from '@/services/dynamicService.service' } from '@/services/dynamicService.service'
import PageTitle from '@/components/shared/PageTitle' import { Container } from '@/components/shared'
import { ROUTES_ENUM } from '@/routes/route.constant'
import Button from '@/components/ui/Button' import Button from '@/components/ui/Button'
import Dialog from '@/components/ui/Dialog'
import { Notification, toast } from '@/components/ui' import { Notification, toast } from '@/components/ui'
import { useConfig } from '@/components/ui/ConfigProvider' import { useConfig } from '@/components/ui/ConfigProvider'
import Input from '@/components/ui/Input' import Input from '@/components/ui/Input'
@ -70,12 +68,12 @@ const INPUT_BASE_CLASS =
'w-full px-3 py-2 border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100' 'w-full px-3 py-2 border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100'
const INPUT_IDLE_BORDER = 'border-slate-300 dark:border-gray-700' const INPUT_IDLE_BORDER = 'border-slate-300 dark:border-gray-700'
const INPUT_ERROR_BORDER = 'border-red-500 bg-red-50 dark:bg-red-900/20' const INPUT_ERROR_BORDER = 'border-red-500 bg-red-50 dark:bg-red-900/20'
const LABEL_CLASS = const LABEL_CLASS = 'block text-sm font-medium text-slate-700 dark:text-gray-300 mb-1'
'block text-sm font-medium text-slate-700 dark:text-gray-300 mb-1'
const getErrorMessage = (error: unknown, fallback: string): string => { const getErrorMessage = (error: unknown, fallback: string): string => {
const response = (error as { response?: { data?: { error?: { message?: string }; message?: string } } }) const response = (
?.response error as { response?: { data?: { error?: { message?: string }; message?: string } } }
)?.response
return ( return (
response?.data?.error?.message || response?.data?.error?.message ||
response?.data?.message || response?.data?.message ||
@ -84,14 +82,22 @@ const getErrorMessage = (error: unknown, fallback: string): string => {
) )
} }
const DynamicServiceEditor: React.FC = () => { /**
const { id } = useParams<{ id: string }>() * Dynamic Service kod editörü. Listeden (`App.DeveloperKit.DynamicServices`) toolbar ve satır
const navigate = useNavigate() * butonlarıyla ılır; diyalog kapandığında grid kendini yeniler, bu yüzden yayınlama başarılı
* olunca diyalog kapatılır.
*/
const DynamicServiceEditor: React.FC<{
open: boolean
onDialogClose: () => void
id?: string
}> = ({ open, onDialogClose, id }) => {
const { translate } = useLocalization() const { translate } = useLocalization()
const { mode } = useConfig() const { mode } = useConfig()
const [code, setCode] = useState(DEFAULT_TEMPLATE) const [code, setCode] = useState(DEFAULT_TEMPLATE)
const [serviceName, setServiceName] = useState('') const [serviceName, setServiceName] = useState('')
const [originalName, setOriginalName] = useState('')
const [displayName, setDisplayName] = useState('') const [displayName, setDisplayName] = useState('')
const [description, setDescription] = useState('') const [description, setDescription] = useState('')
const [primaryEntityType, setPrimaryEntityType] = useState('') const [primaryEntityType, setPrimaryEntityType] = useState('')
@ -134,6 +140,7 @@ const DynamicServiceEditor: React.FC = () => {
if (cancelled) return if (cancelled) return
setCode(data.code || DEFAULT_TEMPLATE) setCode(data.code || DEFAULT_TEMPLATE)
setServiceName(data.name) setServiceName(data.name)
setOriginalName(data.name)
setDisplayName(data.displayName || '') setDisplayName(data.displayName || '')
setDescription(data.description || '') setDescription(data.description || '')
setPrimaryEntityType(data.primaryEntityType || '') setPrimaryEntityType(data.primaryEntityType || '')
@ -162,10 +169,7 @@ const DynamicServiceEditor: React.FC = () => {
} catch (error) { } catch (error) {
const failed: CompileResult = { const failed: CompileResult = {
success: false, success: false,
errorMessage: getErrorMessage( errorMessage: getErrorMessage(error, translate('::App.DynamicServicesEditor.CompileError')),
error,
translate('::App.DynamicServicesEditor.CompileError'),
),
compilationTimeMs: 0, compilationTimeMs: 0,
hasWarnings: false, hasWarnings: false,
errors: [], errors: [],
@ -198,9 +202,10 @@ const DynamicServiceEditor: React.FC = () => {
setIsPublishing(true) setIsPublishing(true)
setPublishResult(null) setPublishResult(null)
try { try {
// Edit modunda eski servis silinip yeniden yayınlanır; silmeden önce // Publish servisi ada göre bulur: ad aynıysa mevcut kayıt güncellenir ve sürüm artar.
// kodun derlendiğinden emin ol ki başarısız derlemede servis kaybolmasın. // Ad değiştiyse eski kayıt kalırdı; onu silmek gerekiyor. Silmeden önce kodun
if (id) { // derlendiğinden emin ol ki başarısız derlemede servis kaybolmasın.
if (id && originalName && originalName !== name) {
const compiled = await runCompile() const compiled = await runCompile()
if (!compiled.success) { if (!compiled.success) {
return return
@ -219,17 +224,15 @@ const DynamicServiceEditor: React.FC = () => {
if (result.success) { if (result.success) {
pushSuccess(translate('::App.Platform.SavedSuccessfully')) pushSuccess(translate('::App.Platform.SavedSuccessfully'))
navigate(ROUTES_ENUM.protected.saas.developerKit.dynamicServices) // Diyalog kapanışı listeyi yeniler.
onDialogClose()
} else { } else {
setPublishResult(result) setPublishResult(result)
} }
} catch (error) { } catch (error) {
setPublishResult({ setPublishResult({
success: false, success: false,
errorMessage: getErrorMessage( errorMessage: getErrorMessage(error, translate('::App.DynamicServicesEditor.PublishError')),
error,
translate('::App.DynamicServicesEditor.PublishError'),
),
}) })
} finally { } finally {
setIsPublishing(false) setIsPublishing(false)
@ -242,7 +245,8 @@ const DynamicServiceEditor: React.FC = () => {
isActive, isActive,
isCompiling, isCompiling,
isPublishing, isPublishing,
navigate, onDialogClose,
originalName,
primaryEntityType, primaryEntityType,
pushSuccess, pushSuccess,
runCompile, runCompile,
@ -259,7 +263,7 @@ const DynamicServiceEditor: React.FC = () => {
} }
}, [code, pushError, pushSuccess, translate]) }, [code, pushError, pushSuccess, translate])
const pageTitle = id const title = id
? translate('::App.DynamicServicesEditor.EditTitle') ? translate('::App.DynamicServicesEditor.EditTitle')
: translate('::App.DeveloperKitDynamicServices.NewService') : translate('::App.DeveloperKitDynamicServices.NewService')
@ -271,302 +275,283 @@ const DynamicServiceEditor: React.FC = () => {
const serviceNameError = submitted && !serviceName.trim() const serviceNameError = submitted && !serviceName.trim()
const isBusy = isCompiling || isPublishing const isBusy = isCompiling || isPublishing
if (isLoading) {
return (
<div className="flex items-center justify-center h-64">
<FaSpinner className="w-8 h-8 animate-spin text-slate-400" />
</div>
)
}
return ( return (
<div className="space-y-4"> <Container>
<PageTitle title={pageTitle} /> <Dialog
width="95%"
{/* Header */} height="90%"
<div className="bg-white dark:bg-gray-900 shadow-lg border-b border-slate-200 dark:border-gray-700 sticky top-0 z-10"> isOpen={open}
<div className="flex items-center justify-between px-1 py-3"> onClose={onDialogClose}
{/* Left: back + icon + title */} onRequestClose={onDialogClose}
<div className="flex items-center gap-3 pl-3"> >
<div className="flex items-center justify-center w-9 h-9 rounded-lg bg-gradient-to-r from-blue-500 to-purple-600 text-white shrink-0"> <Dialog.Body className="flex flex-col">
<FaCode className="w-4 h-4" /> {isLoading ? (
<div className="flex flex-1 items-center justify-center">
<FaSpinner className="w-8 h-8 animate-spin text-slate-400" />
</div> </div>
<div>
<h1 className="font-semibold text-slate-800 dark:text-gray-100 text-sm leading-tight">
{pageTitle}
</h1>
<p className="text-xs text-slate-500 dark:text-gray-400 leading-tight">
{id
? translate('::App.DynamicServicesEditor.EditSubtitle')
: translate('::App.DynamicServicesEditor.NewSubtitle')}
</p>
</div>
</div>
{/* Right: action buttons */}
<div className="flex items-center gap-2 pr-3">
<Link
to={ROUTES_ENUM.protected.saas.developerKit.dynamicServices}
className="flex items-center gap-2 text-slate-600 dark:text-gray-300 px-4 py-2 rounded-lg hover:text-slate-800 dark:hover:text-gray-100 transition-colors"
>
<FaArrowLeft className="w-3.5 h-3.5" />
{translate('::App.DynamicServicesEditor.BackToServices')}
</Link>
<div className="h-6 w-px bg-slate-300 dark:bg-gray-700" />
<Button
type="button"
onClick={copyCode}
variant="plain"
icon={<FaCopy className="h-3.5 w-3.5" />}
>
{translate('::App.DynamicServicesEditor.CopyCode')}
</Button>
<Button
type="button"
onClick={handleTestCompile}
disabled={isBusy || !code.trim()}
variant="solid"
icon={
isCompiling ? (
<FaSpinner className="h-3.5 w-3.5 animate-spin" />
) : (
<FaPlay className="h-3.5 w-3.5" />
)
}
>
{isCompiling
? translate('::App.DynamicServicesEditor.Compiling')
: translate('::App.DynamicServicesEditor.TestCompile')}
</Button>
<Button
type="button"
onClick={handlePublish}
disabled={isBusy || !code.trim()}
variant="solid"
color="green-500"
icon={
isPublishing ? (
<FaSpinner className="h-3.5 w-3.5 animate-spin" />
) : (
<FaSave className="h-3.5 w-3.5" />
)
}
>
{isPublishing
? translate('::App.DynamicServicesEditor.Publishing')
: translate('::App.DynamicServicesEditor.Publish')}
</Button>
</div>
</div>
</div>
{/* Compile / Publish result banners */}
{compileResult && (
<div
className={`flex items-start gap-3 rounded-lg border px-4 py-3 text-sm ${
compileResult.success
? 'bg-emerald-50 dark:bg-emerald-900/20 border-emerald-200 dark:border-emerald-700 text-emerald-800 dark:text-emerald-300'
: 'bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-700 text-red-800 dark:text-red-300'
}`}
>
{compileResult.success ? (
<FaCheckCircle className="w-4 h-4 mt-0.5 shrink-0 text-emerald-600" />
) : ( ) : (
<FaExclamationCircle className="w-4 h-4 mt-0.5 shrink-0 text-red-600" /> <>
<div className="flex items-center gap-3 pb-3 border-b border-slate-200 dark:border-gray-700">
<div className="flex items-center justify-center w-9 h-9 rounded-lg bg-gradient-to-r from-blue-500 to-purple-600 text-white shrink-0">
<FaCode className="w-4 h-4" />
</div>
<div>
<h5 className="font-semibold text-slate-800 dark:text-gray-100 text-sm leading-tight">
{title}
</h5>
<p className="text-xs text-slate-500 dark:text-gray-400 leading-tight">
{id
? translate('::App.DynamicServicesEditor.EditSubtitle')
: translate('::App.DynamicServicesEditor.NewSubtitle')}
</p>
</div>
</div>
{/* Compile / Publish result banners */}
{compileResult && (
<div
className={`mt-3 flex items-start gap-3 rounded-lg border px-4 py-3 text-sm ${
compileResult.success
? 'bg-emerald-50 dark:bg-emerald-900/20 border-emerald-200 dark:border-emerald-700 text-emerald-800 dark:text-emerald-300'
: 'bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-700 text-red-800 dark:text-red-300'
}`}
>
{compileResult.success ? (
<FaCheckCircle className="w-4 h-4 mt-0.5 shrink-0 text-emerald-600" />
) : (
<FaExclamationCircle className="w-4 h-4 mt-0.5 shrink-0 text-red-600" />
)}
<div className="flex-1">
<span className="font-medium">
{compileResult.success
? translate('::App.DynamicServicesEditor.CompileSuccess')
: translate('::App.DynamicServicesEditor.CompileFailed')}
</span>
{!compileResult.success && (
<>
{compileResult.errorMessage && !compileResult.errors?.length && (
<p className="text-xs mt-0.5">{compileResult.errorMessage}</p>
)}
{!!compileResult.errors?.length && (
<ul className="mt-1 space-y-0.5">
{compileResult.errors.map((e, i) => (
<li key={`${e.code}-${e.line}-${i}`} className="text-xs font-mono">
[{e.code}] {translate('::App.DynamicServicesEditor.Line')} {e.line}:{' '}
{e.message}
</li>
))}
</ul>
)}
</>
)}
</div>
<span className="text-xs text-slate-400 shrink-0">
{compileResult.compilationTimeMs}ms
</span>
</div>
)}
{publishResult && !publishResult.success && (
<div className="mt-3 flex items-start gap-3 rounded-lg border bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-700 text-red-800 dark:text-red-300 px-4 py-3 text-sm">
<FaExclamationCircle className="w-4 h-4 mt-0.5 shrink-0 text-red-600" />
<div>
<span className="font-medium">
{translate('::App.DynamicServicesEditor.PublishFailed')}
</span>
{publishResult.errorMessage && (
<p className="text-xs mt-0.5">{publishResult.errorMessage}</p>
)}
</div>
</div>
)}
{/* Two-panel layout */}
<div className="mt-3 flex min-h-0 flex-1 flex-col gap-4 xl:flex-row">
{/* LEFT PANEL — Servis Ayarları */}
<div className="w-full xl:w-1/4 shrink-0 overflow-y-auto bg-white dark:bg-gray-900 rounded-lg border border-slate-200 dark:border-gray-700 p-5 space-y-4">
<div className="flex items-center gap-2 pb-3 border-b border-slate-100 dark:border-gray-700">
<FaCog className="w-4 h-4 text-blue-500" />
<h6 className="font-semibold text-slate-700 dark:text-gray-100 text-sm">
{translate('::App.DynamicServicesEditor.ServiceSettings')}
</h6>
</div>
<div>
<label className={LABEL_CLASS} htmlFor="dse-service-name">
{translate('::App.DynamicServicesEditor.ServiceName')}
</label>
<Input
unstyle
id="dse-service-name"
type="text"
value={serviceName}
onChange={(e) => setServiceName(e.target.value)}
placeholder={translate('::App.DynamicServicesEditor.ServiceNamePlaceholder')}
className={`${INPUT_BASE_CLASS} ${
serviceNameError ? INPUT_ERROR_BORDER : INPUT_IDLE_BORDER
}`}
aria-invalid={serviceNameError}
autoFocus
/>
{serviceNameError && (
<p className="text-red-500 dark:text-red-400 text-xs mt-1">
{translate('::App.DynamicServicesEditor.ServiceNameRequired')}
</p>
)}
</div>
<div>
<label className={LABEL_CLASS} htmlFor="dse-display-name">
{translate('::App.Platform.DisplayName')}
</label>
<Input
unstyle
id="dse-display-name"
type="text"
value={displayName}
onChange={(e) => setDisplayName(e.target.value)}
placeholder={translate('::App.DynamicServicesEditor.DisplayNamePlaceholder')}
className={`${INPUT_BASE_CLASS} ${INPUT_IDLE_BORDER}`}
/>
</div>
<div>
<label className={LABEL_CLASS} htmlFor="dse-description">
{translate('::App.Listform.ListformField.Description')}
</label>
<Input
unstyle
textArea
rows={3}
id="dse-description"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder={translate('::App.DynamicServicesEditor.DescriptionPlaceholder')}
className={`${INPUT_BASE_CLASS} ${INPUT_IDLE_BORDER}`}
/>
</div>
<div>
<label className={LABEL_CLASS} htmlFor="dse-primary-entity">
{translate('::App.DynamicServicesEditor.PrimaryEntityType')}
</label>
<Input
unstyle
id="dse-primary-entity"
type="text"
value={primaryEntityType}
onChange={(e) => setPrimaryEntityType(e.target.value)}
placeholder={translate(
'::App.DynamicServicesEditor.PrimaryEntityTypePlaceholder',
)}
className={`${INPUT_BASE_CLASS} ${INPUT_IDLE_BORDER}`}
/>
</div>
<div className="flex items-center gap-2">
<Input
unstyle
id="dse-is-active"
type="checkbox"
checked={isActive}
onChange={(e) => setIsActive(e.target.checked)}
className="w-4 h-4 rounded accent-blue-600 cursor-pointer"
/>
<label
className="text-sm font-medium text-slate-700 dark:text-gray-300 cursor-pointer"
htmlFor="dse-is-active"
>
{translate('::App.Listform.ListformField.IsActive')}
</label>
</div>
</div>
{/* RIGHT PANEL — Editor */}
<div className="flex w-full min-h-0 min-w-0 flex-1 flex-col">
<div className="flex min-h-0 flex-1 flex-col overflow-hidden rounded-lg border border-slate-200 bg-white dark:border-gray-700 dark:bg-gray-900">
<div className="px-5 py-3 bg-slate-50 dark:bg-gray-800 border-b border-slate-200 dark:border-gray-700 flex items-center justify-between">
<div className="flex items-center gap-2">
<FaCode className="w-4 h-4 text-slate-500 dark:text-gray-400" />
<h6 className="font-medium text-slate-700 dark:text-gray-100 text-sm">
{translate('::App.DynamicServicesEditor.CodeEditor')}
</h6>
</div>
<div className="flex items-center gap-2 text-xs text-slate-500 dark:text-gray-400">
<span>
{translate('::App.DynamicServicesEditor.LineCount')} {lineCount}
</span>
<span className="text-slate-300 dark:text-gray-600">|</span>
<span>
{translate('::App.DynamicServicesEditor.CharCount')} {charCount}
</span>
</div>
</div>
<div className="min-h-0 flex-1">
<Editor
defaultLanguage="csharp"
value={code}
onChange={(value) => setCode(value ?? '')}
options={EDITOR_OPTIONS}
theme={mode === 'dark' ? 'vs-dark' : 'light'}
loading={<FaSpinner className="w-6 h-6 animate-spin text-slate-400" />}
/>
</div>
</div>
</div>
</div>
</>
)} )}
<div className="flex-1"> </Dialog.Body>
<span className="font-medium"> <Dialog.Footer className="flex justify-end items-center gap-2 pt-3 border-t mt-4">
{compileResult.success <Button
? translate('::App.DynamicServicesEditor.CompileSuccess') type="button"
: translate('::App.DynamicServicesEditor.CompileFailed')} onClick={copyCode}
</span> disabled={isLoading}
{!compileResult.success && ( variant="plain"
<> icon={<FaCopy className="h-3.5 w-3.5" />}
{compileResult.errorMessage && !compileResult.errors?.length && ( >
<p className="text-xs mt-0.5">{compileResult.errorMessage}</p> {translate('::App.DynamicServicesEditor.CopyCode')}
)} </Button>
{!!compileResult.errors?.length && (
<ul className="mt-1 space-y-0.5">
{compileResult.errors.map((e, i) => (
<li key={`${e.code}-${e.line}-${i}`} className="text-xs font-mono">
[{e.code}] {translate('::App.DynamicServicesEditor.Line')}{' '}
{e.line}: {e.message}
</li>
))}
</ul>
)}
</>
)}
</div>
<span className="text-xs text-slate-400 shrink-0">
{compileResult.compilationTimeMs}ms
</span>
</div>
)}
{publishResult && !publishResult.success && ( <Button
<div className="flex items-start gap-3 rounded-lg border bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-700 text-red-800 dark:text-red-300 px-4 py-3 text-sm"> type="button"
<FaExclamationCircle className="w-4 h-4 mt-0.5 shrink-0 text-red-600" /> onClick={handleTestCompile}
<div> disabled={isBusy || isLoading || !code.trim()}
<span className="font-medium"> variant="solid"
{translate('::App.DynamicServicesEditor.PublishFailed')} icon={
</span> isCompiling ? (
{publishResult.errorMessage && ( <FaSpinner className="h-3.5 w-3.5 animate-spin" />
<p className="text-xs mt-0.5">{publishResult.errorMessage}</p> ) : (
)} <FaPlay className="h-3.5 w-3.5" />
</div> )
</div> }
)} >
{isCompiling
? translate('::App.DynamicServicesEditor.Compiling')
: translate('::App.DynamicServicesEditor.TestCompile')}
</Button>
{/* Two-panel layout */} <Button
<div className="flex flex-col xl:flex-row gap-4 items-stretch xl:items-start"> type="button"
{/* LEFT PANEL — Servis Ayarları */} onClick={handlePublish}
<div className="w-full xl:w-1/4 shrink-0 bg-white dark:bg-gray-900 rounded-lg border border-slate-200 dark:border-gray-700 p-5 space-y-4"> disabled={isBusy || isLoading || !code.trim()}
<div className="flex items-center gap-2 pb-3 border-b border-slate-100 dark:border-gray-700"> variant="solid"
<FaCog className="w-4 h-4 text-blue-500" /> color="green-500"
<h2 className="font-semibold text-slate-700 dark:text-gray-100 text-sm"> icon={
{translate('::App.DynamicServicesEditor.ServiceSettings')} isPublishing ? (
</h2> <FaSpinner className="h-3.5 w-3.5 animate-spin" />
</div> ) : (
<FaSave className="h-3.5 w-3.5" />
{/* Servis Adı */} )
<div> }
<label className={LABEL_CLASS} htmlFor="dse-service-name"> >
{translate('::App.DynamicServicesEditor.ServiceName')} {isPublishing
</label> ? translate('::App.DynamicServicesEditor.Publishing')
<Input : translate('::App.DynamicServicesEditor.Publish')}
unstyle </Button>
id="dse-service-name" </Dialog.Footer>
type="text" </Dialog>
value={serviceName} </Container>
onChange={(e) => setServiceName(e.target.value)}
placeholder={translate(
'::App.DynamicServicesEditor.ServiceNamePlaceholder',
)}
className={`${INPUT_BASE_CLASS} ${
serviceNameError ? INPUT_ERROR_BORDER : INPUT_IDLE_BORDER
}`}
aria-invalid={serviceNameError}
autoFocus
/>
{serviceNameError && (
<p className="text-red-500 dark:text-red-400 text-xs mt-1">
{translate('::App.DynamicServicesEditor.ServiceNameRequired')}
</p>
)}
</div>
{/* Görünen Ad */}
<div>
<label className={LABEL_CLASS} htmlFor="dse-display-name">
{translate('::App.Platform.DisplayName')}
</label>
<Input
unstyle
id="dse-display-name"
type="text"
value={displayName}
onChange={(e) => setDisplayName(e.target.value)}
placeholder={translate(
'::App.DynamicServicesEditor.DisplayNamePlaceholder',
)}
className={`${INPUT_BASE_CLASS} ${INPUT_IDLE_BORDER}`}
/>
</div>
{/* Açıklama */}
<div>
<label className={LABEL_CLASS} htmlFor="dse-description">
{translate('::App.Listform.ListformField.Description')}
</label>
<Input
unstyle
id="dse-description"
type="text"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder={translate(
'::App.DynamicServicesEditor.DescriptionPlaceholder',
)}
className={`${INPUT_BASE_CLASS} ${INPUT_IDLE_BORDER}`}
/>
</div>
{/* Ana Entity Türü */}
<div>
<label className={LABEL_CLASS} htmlFor="dse-primary-entity">
{translate('::App.DynamicServicesEditor.PrimaryEntityType')}
</label>
<Input
unstyle
id="dse-primary-entity"
type="text"
value={primaryEntityType}
onChange={(e) => setPrimaryEntityType(e.target.value)}
placeholder={translate(
'::App.DynamicServicesEditor.PrimaryEntityTypePlaceholder',
)}
className={`${INPUT_BASE_CLASS} ${INPUT_IDLE_BORDER}`}
/>
</div>
{/* Aktif */}
<div className="flex items-center gap-2">
<Input
unstyle
id="dse-is-active"
type="checkbox"
checked={isActive}
onChange={(e) => setIsActive(e.target.checked)}
className="w-4 h-4 rounded accent-blue-600 cursor-pointer"
/>
<label
className="text-sm font-medium text-slate-700 dark:text-gray-300 cursor-pointer"
htmlFor="dse-is-active"
>
{translate('::App.Listform.ListformField.IsActive')}
</label>
</div>
</div>
{/* RIGHT PANEL — Editor */}
<div className="w-full flex-1 min-w-0">
<div className="bg-white dark:bg-gray-900 rounded-lg border border-slate-200 dark:border-gray-700 overflow-hidden">
<div className="px-5 py-3 bg-slate-50 dark:bg-gray-800 border-b border-slate-200 dark:border-gray-700 flex items-center justify-between">
<div className="flex items-center gap-2">
<FaCode className="w-4 h-4 text-slate-500 dark:text-gray-400" />
<h3 className="font-medium text-slate-700 dark:text-gray-100 text-sm">
{translate('::App.DynamicServicesEditor.CodeEditor')}
</h3>
</div>
<div className="flex items-center gap-2 text-xs text-slate-500 dark:text-gray-400">
<span>
{translate('::App.DynamicServicesEditor.LineCount')} {lineCount}
</span>
<span className="text-slate-300 dark:text-gray-600">|</span>
<span>
{translate('::App.DynamicServicesEditor.CharCount')} {charCount}
</span>
</div>
</div>
<div className="h-[560px]">
<Editor
defaultLanguage="csharp"
value={code}
onChange={(value) => setCode(value ?? '')}
options={EDITOR_OPTIONS}
theme={mode === 'dark' ? 'vs-dark' : 'light'}
loading={
<FaSpinner className="w-6 h-6 animate-spin text-slate-400" />
}
/>
</div>
</div>
</div>
</div>
</div>
) )
} }

View file

@ -1,422 +0,0 @@
import React, { useState, useEffect } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import {
FaPlus,
FaSearch,
FaRegEdit,
FaTrashAlt,
FaFilter,
FaCalendarAlt,
FaCode,
FaSpinner,
FaExternalLinkAlt,
FaTh,
FaList,
} from 'react-icons/fa'
import Widget from '@/components/common/Widget'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { dynamicServiceService, type DynamicServiceDto } from '@/services/dynamicService.service'
import PageTitle from '@/components/shared/PageTitle'
import { ROUTES_ENUM } from '@/routes/route.constant'
import Button from '@/components/ui/Button'
import Input from '@/components/ui/Input'
import Select from '@/components/ui/Select'
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-dynamicservices'
const DynamicServiceManager: React.FC = () => {
const { translate } = useLocalization()
const navigate = useNavigate()
const filterStatusOptions = [
{ value: 'all' as const, label: translate('::App.StaticLookup.All') },
{ value: 'Success' as const, label: translate('::App.Platform.Success') },
{ value: 'Failed' as const, label: translate('::App.Platform.Failed') },
{
value: 'Pending' as const,
label: translate('::App.DeveloperKitDynamicServices.FilterPending'),
},
]
const [services, setServices] = useState<DynamicServiceDto[]>([])
const [isLoading, setIsLoading] = useState(false)
const [searchTerm, setSearchTerm] = useState('')
const [filterStatus, setFilterStatus] = useState<'all' | 'Success' | 'Failed' | 'Pending'>('all')
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 })
useEffect(() => {
loadServices()
}, [])
const loadServices = async () => {
try {
setIsLoading(true)
const response = await dynamicServiceService.getList()
setServices(response.items || [])
} catch (error) {
console.error('Servisler yüklenirken hata:', error)
} finally {
setIsLoading(false)
}
}
const deleteService = async (serviceId: string) => {
if (!confirm(translate('::App.DeveloperKitDynamicServices.DeleteConfirm'))) return
try {
await dynamicServiceService.delete(serviceId)
await loadServices()
} catch (error) {
console.error('Servis silinirken hata:', error)
alert(translate('::App.DeveloperKitDynamicServices.DeleteError'))
}
}
const totalServices = services.length
const successServices = services.filter((s) => s.compilationStatus === 'Success').length
const failedServices = services.filter((s) => s.compilationStatus === 'Failed').length
const activeServices = services.filter((s) => s.isActive).length
const inactiveServices = services.filter((s) => !s.isActive).length
const filteredServices = services.filter((service) => {
const matchesSearch =
service.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
(service.displayName || '').toLowerCase().includes(searchTerm.toLowerCase())
const matchesFilter = filterStatus === 'all' || service.compilationStatus === filterStatus
return matchesSearch && matchesFilter
})
const statusBadge = (status: string) => {
if (status === 'Success')
return 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300'
if (status === 'Failed') return 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300'
return 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-300'
}
const openSwagger = () => {
window.open(`${import.meta.env.VITE_API_URL}/swagger/index.html`, '_blank')
}
const renderStatusBadge = (service: DynamicServiceDto) => (
<span
className={`inline-block px-2 py-0.5 text-xs rounded-full font-medium whitespace-nowrap ${statusBadge(
service.compilationStatus,
)}`}
>
{service.compilationStatus} · v{service.version}
</span>
)
const renderActions = (service: DynamicServiceDto) => (
<div className="flex items-center gap-1">
<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={() =>
navigate(
ROUTES_ENUM.protected.saas.developerKit.dynamicServicesEdit.replace(':id', service.id),
)
}
/>
<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={() => deleteService(service.id)}
/>
</div>
)
return (
<div className="space-y-4">
<PageTitle title={translate('::' + 'App.DeveloperKit.DynamicServices')} />
{/* Stats */}
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-5 gap-4 mt-2 mb-4">
<Widget
title={translate('::App.Listform.ListformField.Total')}
value={totalServices}
color="purple"
icon="FaCode"
valueClassName="text-3xl"
subTitle={translate('::App.DeveloperKitDynamicServices.TotalDescription')}
/>
<Widget
title={translate('::App.Platform.Success')}
value={successServices}
color="green"
icon="FaCheckCircle"
valueClassName="text-3xl"
subTitle={translate('::App.DeveloperKitDynamicServices.SuccessfulDescription')}
/>
<Widget
title={translate('::App.Platform.Failed')}
value={failedServices}
color="red"
icon="FaTimesCircle"
valueClassName="text-3xl"
subTitle={translate('::App.DeveloperKitDynamicServices.TotalDescription')}
/>
<Widget
title={translate('::App.Listform.ListformField.IsActive')}
value={activeServices}
color="teal"
icon="FaArrowUp"
valueClassName="text-3xl"
subTitle={translate('::App.DeveloperKitDynamicServices.ActiveDescription')}
/>
<Widget
title={translate('::App.Platform.Passive')}
value={inactiveServices}
color="gray"
icon="FaArrowDown"
valueClassName="text-3xl"
subTitle={translate('::App.DeveloperKitDynamicServices.PassiveDescription')}
/>
</div>
{/* Toolbar */}
<div className="flex flex-col lg:flex-row lg:items-center gap-3">
<div className="flex-1 relative">
<FaSearch className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
<Input
unstyle
type="text"
placeholder={translate('::App.DeveloperKitDynamicServices.SearchPlaceholder')}
value={searchTerm}
className="w-full pl-10 pr-4 py-2 border border-slate-300 dark:border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100"
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
<div className="flex items-center gap-2 w-full lg:w-auto">
<FaFilter className="w-4 h-4 text-slate-500 dark:text-gray-400" />
<Select
className="w-full lg:w-48"
options={filterStatusOptions}
value={filterStatusOptions.filter((o) => o.value === filterStatus)}
onChange={(option) => setFilterStatus(option?.value ?? 'all')}
/>
</div>
<div className="w-full sm:w-auto">
<Button
type="button"
variant="solid"
color="green-600"
icon={<FaExternalLinkAlt className="h-3 w-3" />}
className="w-full !h-auto !items-center !justify-center gap-2 whitespace-nowrap !rounded-lg px-4 py-2 text-white transition-colors hover:!bg-green-700 dark:!bg-green-700 dark:hover:!bg-green-800 sm:w-auto"
onClick={openSwagger}
>
Swagger
</Button>
</div>
<div className="w-full sm:w-auto">
<Link
to={ROUTES_ENUM.protected.saas.developerKit.dynamicServicesNew}
className="w-full sm:w-auto justify-center flex items-center gap-2 bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 dark:bg-blue-700 dark:hover:bg-blue-800 transition-colors"
>
<FaPlus className="w-4 h-4" />
{translate('::App.DeveloperKitDynamicServices.NewService')}
</Link>
</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>
{/* List */}
{isLoading ? (
<div className="flex items-center justify-center h-40">
<FaSpinner className="w-8 h-8 animate-spin text-slate-400" />
</div>
) : filteredServices.length > 0 ? (
viewMode === 'card' ? (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{filteredServices.map((service) => (
<div
key={service.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={service.name}
>
{service.name}
</h3>
<div
className={`w-2 h-2 shrink-0 rounded-full ${
service.isActive ? 'bg-emerald-500' : 'bg-slate-300 dark:bg-gray-700'
}`}
/>
</div>
{service.displayName && (
<p
className="text-slate-600 dark:text-gray-300 text-sm mb-2 truncate"
title={service.displayName}
>
{service.displayName}
</p>
)}
<div className="mb-2">{renderStatusBadge(service)}</div>
{service.description && (
<p className="text-slate-600 dark:text-gray-300 text-sm mb-2 line-clamp-2">
{service.description}
</p>
)}
</div>
{/* Sag taraf */}
{service.lastSuccessfulCompilation && (
<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(service.lastSuccessfulCompilation).toLocaleDateString()}
</span>
</div>
)}
</div>
{/* Actions */}
<div className="flex items-center justify-end gap-1 pt-2 border-t border-slate-100 dark:border-gray-700">
{renderActions(service)}
</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.DisplayName')}
</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.Listform.ListformField.Version')}
</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>
{filteredServices.map((service) => (
<tr
key={service.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 ${
service.isActive ? 'bg-emerald-500' : 'bg-slate-300 dark:bg-gray-700'
}`}
/>
<span className="font-medium text-slate-900 dark:text-gray-100">
{service.name}
</span>
</div>
</td>
<td className="px-4 py-2 text-slate-600 dark:text-gray-300">
{service.displayName}
</td>
<td className="px-4 py-2 text-slate-600 dark:text-gray-300">
{service.description}
</td>
<td className="px-4 py-2 text-slate-600 dark:text-gray-300 whitespace-nowrap">
v{service.version}
</td>
<td className="px-4 py-2 text-slate-500 dark:text-gray-400 whitespace-nowrap">
{service.lastSuccessfulCompilation
? new Date(service.lastSuccessfulCompilation).toLocaleDateString()
: ''}
</td>
<td className="px-4 py-2">{renderStatusBadge(service)}</td>
<td className="px-4 py-2">
<div className="flex justify-end">{renderActions(service)}</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)
) : (
<div className="text-center py-12">
<div className="bg-slate-100 dark:bg-gray-800 rounded-full w-16 h-16 flex items-center justify-center mx-auto mb-4">
<FaCode className="w-8 h-8 text-slate-400 dark:text-gray-500" />
</div>
<h3 className="text-lg font-medium text-slate-900 dark:text-gray-100 mb-2">
{searchTerm || filterStatus !== 'all'
? translate('::App.Platform.NoResults')
: translate('::App.DeveloperKitDynamicServices.NoServicesYet')}
</h3>
<p className="text-slate-500 dark:text-gray-400 mb-6">
{searchTerm || filterStatus !== 'all'
? translate('::App.DeveloperKitDynamicServices.TryChangingFilter')
: translate('::App.DeveloperKitDynamicServices.GetStarted')}
</p>
{!searchTerm && filterStatus === 'all' && (
<Link
to={ROUTES_ENUM.protected.saas.developerKit.dynamicServicesNew}
className="inline-flex items-center gap-2 bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 dark:bg-blue-700 dark:hover:bg-blue-800 transition-colors"
>
<FaPlus className="w-4 h-4" />
{translate('::App.DeveloperKitDynamicServices.CreateNewService')}
</Link>
)}
</div>
)}
</div>
)
}
export default DynamicServiceManager

View file

@ -5,6 +5,41 @@
import type { GridDto } from '@/proxy/form/models' import type { GridDto } from '@/proxy/form/models'
import { controlStyleCondition } from '../Utils' import { controlStyleCondition } from '../Utils'
/**
* DevExtreme, jQuery yüklüyse jQuery nesnesi, aksi hâlde düz DOM elemanı verir; ikisi de desteklenir.
*/
const addClass = (cellElement: any, cssClassName: string) => {
if (typeof cellElement?.addClass === 'function') {
cellElement.addClass(cssClassName)
return
}
cellElement?.classList?.add(...cssClassName.split(/\s+/).filter(Boolean))
}
/**
* `CssStyles` yazım biçimi `anahtar:değer | anahtar:değer`'dir; inline style'a eklenirken
* ayraç `;` olmalıdır.
*/
const appendStyles = (cellElement: any, cssStyles: string) => {
const declarations = cssStyles
.split('|')
.map((declaration) => declaration.trim())
.filter(Boolean)
.join(';')
if (!declarations) return
if (typeof cellElement?.attr === 'function') {
const current = cellElement.attr('style')
cellElement.attr('style', current ? `${current};${declarations}` : declarations)
return
}
if (cellElement?.style) {
const current = cellElement.style.cssText
cellElement.style.cssText = current ? `${current};${declarations}` : declarations
}
}
/** /**
* `onCellPrepared` handler'ı üretir. Yapılandırılmış stil bulunmayan * `onCellPrepared` handler'ı üretir. Yapılandırılmış stil bulunmayan
* liste formlarında hiç yapmaması için önceden filtreleme yapılır. * liste formlarında hiç yapmaması için önceden filtreleme yapılır.
@ -28,10 +63,10 @@ export const createConditionalCellStyleHandler = (gridDto?: GridDto) => {
if (colStyle.conditionValue && !controlStyleCondition(e.data, fieldName, colStyle)) return if (colStyle.conditionValue && !controlStyleCondition(e.data, fieldName, colStyle)) return
if (colStyle.cssClassName) { if (colStyle.cssClassName) {
e.cellElement.addClass(colStyle.cssClassName) addClass(e.cellElement, colStyle.cssClassName)
} }
if (colStyle.cssStyles) { if (colStyle.cssStyles) {
e.cellElement.attr('style', `${e.cellElement.attr('style')};${colStyle.cssStyles}`) appendStyles(e.cellElement, colStyle.cssStyles)
} }
}) })
}) })

View file

@ -14,6 +14,7 @@ import { useStoreState } from '@/store'
import { workflowService } from '@/services/workflow.service' import { workflowService } from '@/services/workflow.service'
import type { WorkflowRunResultDto } from '@/services/workflow.service' import type { WorkflowRunResultDto } from '@/services/workflow.service'
import { splitWorkflowApprovers, translateWorkflowLabel } from '@/utils/workflow/workflowHelpers' import { splitWorkflowApprovers, translateWorkflowLabel } from '@/utils/workflow/workflowHelpers'
import { safeJsonParse } from './Utils'
type ToolbarModalData = { type ToolbarModalData = {
open: boolean open: boolean
@ -105,7 +106,27 @@ const useToolbar = ({
// Add InsertNewRecord button // Add InsertNewRecord button
if (grdOpt.editingOptionDto?.allowAdding && checkPermission(grdOpt.permissionDto?.c)) { if (grdOpt.editingOptionDto?.allowAdding && checkPermission(grdOpt.permissionDto?.c)) {
if (grdOpt.editingOptionDto?.addPageUrl) { if (grdOpt.editingOptionDto?.addDialogName) {
items.push({
widget: 'dxButton',
showText: 'always',
name: 'addRowButton',
location: 'after',
options: {
icon: 'add',
text: translateToolbar('::App.Platform.Add'),
hint: translate('::App.Platform.Add'),
onClick() {
dialog.setConfig({
component: grdOpt.editingOptionDto?.addDialogName,
props: safeJsonParse(grdOpt.editingOptionDto?.addDialogParameters, {}),
// Diyalog kapaninca liste yenilenir; yeni kayit hemen gorunur.
onClose: () => refreshData(),
})
},
},
})
} else if (grdOpt.editingOptionDto?.addPageUrl) {
items.push({ items.push({
widget: 'dxButton', widget: 'dxButton',
showText: 'always', showText: 'always',
@ -683,6 +704,8 @@ const useToolbar = ({
dialog.setConfig({ dialog.setConfig({
component: action.dialogName, component: action.dialogName,
props: dynamicMap, props: dynamicMap,
// Satir butonuyla ayni davranis: diyalog kapaninca liste yenilenir.
onClose: () => refreshData(),
}) })
} }
} else if (action.onClick) { } else if (action.onClick) {

View file

@ -6,6 +6,7 @@ import AuditLogDetail from '@/views/admin/auditLog/AuditLogDetail'
import RolesPermission from '@/views/admin/role-management/RolesPermission' import RolesPermission from '@/views/admin/role-management/RolesPermission'
import UsersPermission from '@/views/admin/user-management/UsersPermission' import UsersPermission from '@/views/admin/user-management/UsersPermission'
import BranchSeed from '@/views/branch/BranchSeed' import BranchSeed from '@/views/branch/BranchSeed'
import DynamicServiceEditor from '@/views/developerKit/DynamicServiceEditor'
import { ReactElement } from 'react' import { ReactElement } from 'react'
const DialogShowComponent = (): ReactElement => { const DialogShowComponent = (): ReactElement => {
@ -68,6 +69,14 @@ const DialogShowComponent = (): ReactElement => {
{...dialogContext.config?.props} {...dialogContext.config?.props}
></AuditLogDetail> ></AuditLogDetail>
) )
case 'DynamicServiceEditor':
return (
<DynamicServiceEditor
open={true}
onDialogClose={handleDialogClose}
{...dialogContext.config?.props}
></DynamicServiceEditor>
)
case 'BranchSeed': case 'BranchSeed':
return ( return (
<BranchSeed <BranchSeed