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ç |
| `EditColumnName` | — | Aksiyon sütununun başlığı |
| `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` (`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 |
| --- | --- |
| `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) |
| `ValueClassName` | Değerin CSS sınıfı, varsayılan `text-3xl` |
| `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. |
| **Developer Kit SQL Query Manager** | `App.SqlQueryManager` · `/admin/sqlQueryManager` | — | — | View/procedure/function yazma ve çalıştırma; karar sırasının 2. adımı buradan başlar. |
| **Developer Kit Custom Endpoints** | `App.DeveloperKit.CustomEndpoints` | `Sas_H_CustomEndpoint` | tenant | `Name`, `Url`, `Method`, `DataSourceCode`, `Sql`, `ParametersJson`, `PermissionsJson` — parametreli SQL'i yetkili bir HTTP ucuna çevirir. |
| **Developer Kit Dynamic Services** | `App.DeveloperKit.DynamicServices` · `/admin/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 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`. |
@ -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/edit/:listFormCode` | `listForm/edit/FormEdit` | `[]` — yetki ListForm tarafında |
| `/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/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` |
@ -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) |
| 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 |
| 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}"` |
| Toplam satırı | `Sales Order Item`, `Licence Item` (`TotalSummaryJson`) |
| 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.
- `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ı
açmana gerek yok.
- 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
`@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.
- **Diyalog modu satır butonu içindir.** `@` ile başlayan parametreler satır verisinden okunur;
toolbar'da satır bağlamı olmadığı için yalnızca sabit parametrelerle çalışır. Satır butonunda
diyalog kapanınca grid otomatik yenilenir. `DialogParameters` boşsa diyalog hiç açılmaz.
- **Diyalog modunda `@` parametreleri satır butonuna aittir**: değerleri satır verisinden okunur,
toolbar'da satır bağlamı olmadığı için orada yalnızca sabit parametreler çalışır (yeni kayıt
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ı
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
`/admin/list/{ListFormCode}`).
- 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ı
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ığı
`/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
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
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 |
| **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 |
| **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 |
@ -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.ApiClearRedisCache()` | Dağıtık önbelleği temizler | — |
| `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` |
### 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**. |
| **Custom Endpoint** | `/admin/list/App.DeveloperKit.CustomEndpoints` | SQL veya servis tanımından REST endpoint üretimi; GET/POST/PUT/DELETE için ayrı yetki kapıları (`App.DeveloperKit.CustomEndpoints.*`) ve kullanıcı/rol/global erişim kuralları. |
| **CRUD Endpoint** | SQL Query Manager içinde | Tablo tanımından tam CRUD endpoint kümesi üretimi. Ayrı ekranı yoktur; bkz. aşağıdaki not. |
| **Dynamic Service** | `/admin/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}`. |
| **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. |

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
/// </summary>
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",
"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",
"key": "App.ListFormEdit.EditingAllowDetail",
@ -17919,9 +17931,57 @@
{
"resourceName": "Platform",
"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",
"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",
"key": "App.DeveloperKitDynamicServices.ActiveDescription",
@ -17934,12 +17994,6 @@
"en": "Number of passive services",
"tr": "Pasif servislerin sayısı"
},
{
"resourceName": "Platform",
"key": "App.DeveloperKitDynamicServices.SearchPlaceholder",
"en": "Search service...",
"tr": "Servis ara..."
},
{
"resourceName": "Platform",
"key": "App.DeveloperKitDynamicServices.FilterPending",
@ -17952,30 +18006,6 @@
"en": "New Service",
"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",
"key": "App.DeveloperKitDynamicServices.DeleteConfirm",

View file

@ -328,30 +328,6 @@
"routeType": "protected",
"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",
"path": "/admin/developerkit/components",
@ -1013,7 +989,7 @@
"Code": "App.DeveloperKit.DynamicServices",
"DisplayName": "App.DeveloperKit.DynamicServices",
"Order": 4,
"Url": "/admin/developerkit/dynamic-services",
"Url": "/admin/list/App.DeveloperKit.DynamicServices",
"Icon": "FcCommandLine",
"RequiredPermissionName": "App.DeveloperKit.DynamicServices",
"IsDisabled": false,

View file

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

View file

@ -46,7 +46,7 @@ function DbMigrateLogPanel({ onClose }: DbMigrateLogPanelProps) {
return (
<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">
<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 { WidgetEditDto, WidgetGroupDto } from '@/proxy/form/models'
import { useNavigate } from 'react-router-dom'
import { useLocalization } from '@/utils/hooks/useLocalization'
export default function WidgetGroup({ widgetGroups }: { widgetGroups: WidgetGroupDto[] }) {
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 (
<div>
@ -28,11 +34,11 @@ export default function WidgetGroup({ widgetGroups }: { widgetGroups: WidgetGrou
style={{ gridColumn: `span ${group.colSpan} / span ${group.colSpan}` }}
>
<Widget
title={item.title}
title={asText(item.title) as string}
value={item.value}
color={item.color as colorType}
icon={item.icon}
subTitle={item.subTitle}
subTitle={asText(item.subTitle)}
valueClassName={item.valueClassName}
onClick={() => {
if (item.onClick) {

View file

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

View file

@ -41,9 +41,6 @@
entitiesEdit: '/admin/developerkit/entities/edit/:id',
components: '/admin/developerkit/components',
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: {
view: '/admin/reports/:report/view/:id?/:listFormCode?',

View file

@ -4,6 +4,7 @@ import { getLocalization } from '@/services/localization.service'
import { store } from '@/store'
import { clearRedisCache } from './languageText.service'
import { kickUser } from './identity.service'
import { dynamicServiceService } from './dynamicService.service'
import { streamSetupMigrate } from './setup.service'
import { openDbMigrateLogPanel, dispatchMigrateLog, dispatchMigrateDone } from '@/components/shared/DbMigrateLogPanel'
@ -54,6 +55,39 @@ export abstract class UiEvalService {
}, '::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).
* Yetki sunucu tarafında `App.Setup.Migrate` izniyle doğrulanır.

View file

@ -57,25 +57,9 @@ export interface PublishDto {
isActive?: boolean
}
export interface DynamicAppServiceListResult {
items: DynamicServiceDto[]
totalCount: number
}
class DynamicServiceService {
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
*/
@ -87,30 +71,6 @@ class DynamicServiceService {
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
*/
@ -144,28 +104,6 @@ class DynamicServiceService {
})
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()

View file

@ -75,6 +75,38 @@ function FormTabEdit(props: FormEditProps & { listFormCode: string }) {
component={Input}
/>
</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
label={translate('::App.ListFormEdit.AllowUpdating')}
invalid={
@ -135,6 +167,23 @@ function FormTabEdit(props: FormEditProps & { listFormCode: string }) {
component={Checkbox}
/>
</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
label={translate('::App.ListFormEdit.EditingAllowDeleting')}
invalid={
@ -163,23 +212,8 @@ function FormTabEdit(props: FormEditProps & { listFormCode: string }) {
component={Checkbox}
/>
</FormItem>
</div>
<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
label={translate('::App.ListFormEdit.EditingUseIcons')}
invalid={

View file

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

View file

@ -1,5 +1,4 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { useParams, useNavigate, Link } from 'react-router-dom'
import { Editor } from '@monaco-editor/react'
import type { editor } from 'monaco-editor'
import {
@ -8,7 +7,6 @@ import {
FaCheckCircle,
FaExclamationCircle,
FaSpinner,
FaArrowLeft,
FaCog,
FaCode,
FaSave,
@ -19,9 +17,9 @@ import {
type CompileResult,
type PublishResult,
} from '@/services/dynamicService.service'
import PageTitle from '@/components/shared/PageTitle'
import { ROUTES_ENUM } from '@/routes/route.constant'
import { Container } from '@/components/shared'
import Button from '@/components/ui/Button'
import Dialog from '@/components/ui/Dialog'
import { Notification, toast } from '@/components/ui'
import { useConfig } from '@/components/ui/ConfigProvider'
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'
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 LABEL_CLASS =
'block text-sm font-medium text-slate-700 dark:text-gray-300 mb-1'
const LABEL_CLASS = 'block text-sm font-medium text-slate-700 dark:text-gray-300 mb-1'
const getErrorMessage = (error: unknown, fallback: string): string => {
const response = (error as { response?: { data?: { error?: { message?: string }; message?: string } } })
?.response
const response = (
error as { response?: { data?: { error?: { message?: string }; message?: string } } }
)?.response
return (
response?.data?.error?.message ||
response?.data?.message ||
@ -84,14 +82,22 @@ const getErrorMessage = (error: unknown, fallback: string): string => {
)
}
const DynamicServiceEditor: React.FC = () => {
const { id } = useParams<{ id: string }>()
const navigate = useNavigate()
/**
* Dynamic Service kod editörü. Listeden (`App.DeveloperKit.DynamicServices`) toolbar ve satır
* 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 { mode } = useConfig()
const [code, setCode] = useState(DEFAULT_TEMPLATE)
const [serviceName, setServiceName] = useState('')
const [originalName, setOriginalName] = useState('')
const [displayName, setDisplayName] = useState('')
const [description, setDescription] = useState('')
const [primaryEntityType, setPrimaryEntityType] = useState('')
@ -134,6 +140,7 @@ const DynamicServiceEditor: React.FC = () => {
if (cancelled) return
setCode(data.code || DEFAULT_TEMPLATE)
setServiceName(data.name)
setOriginalName(data.name)
setDisplayName(data.displayName || '')
setDescription(data.description || '')
setPrimaryEntityType(data.primaryEntityType || '')
@ -162,10 +169,7 @@ const DynamicServiceEditor: React.FC = () => {
} catch (error) {
const failed: CompileResult = {
success: false,
errorMessage: getErrorMessage(
error,
translate('::App.DynamicServicesEditor.CompileError'),
),
errorMessage: getErrorMessage(error, translate('::App.DynamicServicesEditor.CompileError')),
compilationTimeMs: 0,
hasWarnings: false,
errors: [],
@ -198,9 +202,10 @@ const DynamicServiceEditor: React.FC = () => {
setIsPublishing(true)
setPublishResult(null)
try {
// Edit modunda eski servis silinip yeniden yayınlanır; silmeden önce
// kodun derlendiğinden emin ol ki başarısız derlemede servis kaybolmasın.
if (id) {
// Publish servisi ada göre bulur: ad aynıysa mevcut kayıt güncellenir ve sürüm artar.
// Ad değiştiyse eski kayıt kalırdı; onu silmek gerekiyor. Silmeden önce kodun
// derlendiğinden emin ol ki başarısız derlemede servis kaybolmasın.
if (id && originalName && originalName !== name) {
const compiled = await runCompile()
if (!compiled.success) {
return
@ -219,17 +224,15 @@ const DynamicServiceEditor: React.FC = () => {
if (result.success) {
pushSuccess(translate('::App.Platform.SavedSuccessfully'))
navigate(ROUTES_ENUM.protected.saas.developerKit.dynamicServices)
// Diyalog kapanışı listeyi yeniler.
onDialogClose()
} else {
setPublishResult(result)
}
} catch (error) {
setPublishResult({
success: false,
errorMessage: getErrorMessage(
error,
translate('::App.DynamicServicesEditor.PublishError'),
),
errorMessage: getErrorMessage(error, translate('::App.DynamicServicesEditor.PublishError')),
})
} finally {
setIsPublishing(false)
@ -242,7 +245,8 @@ const DynamicServiceEditor: React.FC = () => {
isActive,
isCompiling,
isPublishing,
navigate,
onDialogClose,
originalName,
primaryEntityType,
pushSuccess,
runCompile,
@ -259,7 +263,7 @@ const DynamicServiceEditor: React.FC = () => {
}
}, [code, pushError, pushSuccess, translate])
const pageTitle = id
const title = id
? translate('::App.DynamicServicesEditor.EditTitle')
: translate('::App.DeveloperKitDynamicServices.NewService')
@ -271,302 +275,283 @@ const DynamicServiceEditor: React.FC = () => {
const serviceNameError = submitted && !serviceName.trim()
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 (
<div className="space-y-4">
<PageTitle title={pageTitle} />
{/* Header */}
<div className="bg-white dark:bg-gray-900 shadow-lg border-b border-slate-200 dark:border-gray-700 sticky top-0 z-10">
<div className="flex items-center justify-between px-1 py-3">
{/* Left: back + icon + title */}
<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">
<FaCode className="w-4 h-4" />
<Container>
<Dialog
width="95%"
height="90%"
isOpen={open}
onClose={onDialogClose}
onRequestClose={onDialogClose}
>
<Dialog.Body className="flex flex-col">
{isLoading ? (
<div className="flex flex-1 items-center justify-center">
<FaSpinner className="w-8 h-8 animate-spin text-slate-400" />
</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">
<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>
)}
</Dialog.Body>
<Dialog.Footer className="flex justify-end items-center gap-2 pt-3 border-t mt-4">
<Button
type="button"
onClick={copyCode}
disabled={isLoading}
variant="plain"
icon={<FaCopy className="h-3.5 w-3.5" />}
>
{translate('::App.DynamicServicesEditor.CopyCode')}
</Button>
{publishResult && !publishResult.success && (
<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">
<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>
)}
<Button
type="button"
onClick={handleTestCompile}
disabled={isBusy || isLoading || !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>
{/* Two-panel layout */}
<div className="flex flex-col xl:flex-row gap-4 items-stretch xl:items-start">
{/* LEFT PANEL — Servis Ayarları */}
<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">
<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" />
<h2 className="font-semibold text-slate-700 dark:text-gray-100 text-sm">
{translate('::App.DynamicServicesEditor.ServiceSettings')}
</h2>
</div>
{/* Servis Adı */}
<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>
{/* 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>
<Button
type="button"
onClick={handlePublish}
disabled={isBusy || isLoading || !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>
</Dialog.Footer>
</Dialog>
</Container>
)
}

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 { 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
* 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.cssClassName) {
e.cellElement.addClass(colStyle.cssClassName)
addClass(e.cellElement, colStyle.cssClassName)
}
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 type { WorkflowRunResultDto } from '@/services/workflow.service'
import { splitWorkflowApprovers, translateWorkflowLabel } from '@/utils/workflow/workflowHelpers'
import { safeJsonParse } from './Utils'
type ToolbarModalData = {
open: boolean
@ -105,7 +106,27 @@ const useToolbar = ({
// Add InsertNewRecord button
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({
widget: 'dxButton',
showText: 'always',
@ -683,6 +704,8 @@ const useToolbar = ({
dialog.setConfig({
component: action.dialogName,
props: dynamicMap,
// Satir butonuyla ayni davranis: diyalog kapaninca liste yenilenir.
onClose: () => refreshData(),
})
}
} 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 UsersPermission from '@/views/admin/user-management/UsersPermission'
import BranchSeed from '@/views/branch/BranchSeed'
import DynamicServiceEditor from '@/views/developerKit/DynamicServiceEditor'
import { ReactElement } from 'react'
const DialogShowComponent = (): ReactElement => {
@ -68,6 +69,14 @@ const DialogShowComponent = (): ReactElement => {
{...dialogContext.config?.props}
></AuditLogDetail>
)
case 'DynamicServiceEditor':
return (
<DynamicServiceEditor
open={true}
onDialogClose={handleDialogClose}
{...dialogContext.config?.props}
></DynamicServiceEditor>
)
case 'BranchSeed':
return (
<BranchSeed