Dynamic Services için LsitForm tanımlandı
This commit is contained in:
parent
76091c0518
commit
54e3390cbf
21 changed files with 12833 additions and 12231 deletions
|
|
@ -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`) |
|
||||
|
|
|
|||
42
.github/instructions/lowcode.instructions.md
vendored
42
.github/instructions/lowcode.instructions.md
vendored
|
|
@ -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
|
||||
açı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ı
|
||||
|
|
|
|||
|
|
@ -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. |
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
{/*
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -497,6 +497,8 @@ export interface GridEditingDto {
|
|||
isBranch: boolean
|
||||
isOrganizationUnit: boolean
|
||||
addPageUrl?: string
|
||||
addDialogName?: string
|
||||
addDialogParameters?: string
|
||||
}
|
||||
|
||||
export interface EditingFormDto {
|
||||
|
|
|
|||
|
|
@ -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?',
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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={
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 açı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>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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ç iş 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)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue