From 9981d5b19a03dbd93990c04d42c04b8d6eadbd6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sedat=20=C3=96ZT=C3=9CRK?= <76204082+iamsedatozturk@users.noreply.github.com> Date: Wed, 26 Aug 2026 21:44:53 +0300 Subject: [PATCH] =?UTF-8?q?claude=20instructions=20dosyalar=C4=B1=20d?= =?UTF-8?q?=C3=BCzenlendi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/instructions/ai.instructions.md | 184 +- .github/instructions/list.instructions.md | 4 + .github/instructions/lowcode.instructions.md | 2064 +++++++++++++++++ README.md | 334 ++- claude.md | 76 + ui/src/constants/actionButton.constant.ts | 12 + ui/src/sw.ts | 23 +- .../listForm/wizard/WizardFileManager.tsx | 41 +- .../views/developerKit/ComponentManager.tsx | 41 +- .../developerKit/DynamicServiceManager.tsx | 275 ++- .../developerKit/VisualComponentDesigner.tsx | 254 +- ui/src/views/version/swRegistration.ts | 84 +- 12 files changed, 3104 insertions(+), 288 deletions(-) create mode 100644 .github/instructions/lowcode.instructions.md create mode 100644 ui/src/constants/actionButton.constant.ts diff --git a/.github/instructions/ai.instructions.md b/.github/instructions/ai.instructions.md index a1597157..83465758 100644 --- a/.github/instructions/ai.instructions.md +++ b/.github/instructions/ai.instructions.md @@ -14,6 +14,16 @@ Primary principle: - Configuration first, code last. +Companion documents (this file wins on conflict): + +| Document | Scope | +| --- | --- | +| `lowcode.instructions.md` | **Authoring reference**: exact schemas, enum values and worked examples for producing wizard screens, editor scripts, custom components, SQL objects and endpoints. Read it before writing any artifact. | +| `dotnet.instructions.md` | Binding code standard for everything under `api/` | +| `list.instructions.md` | Step-by-step procedure for adding a module / list through seeders | +| `../../README.md` | Technical map: what exists, where it lives, how it is wired | +| `../../CLAUDE.md` | Operating rules and standing defaults for AI agents in this repo | + --- ## 2. Platform Scope @@ -116,7 +126,7 @@ Capabilities: - Query - WebService - Cascading lookup parent-child behavior -- Dynamic validation, editor options, editor scripts +- Dynamic validation, editor options, editor scripts (see 7.1) - Conditional formatting and style injection - Grid state save/load/reset - User filter save/apply/delete flows @@ -146,6 +156,34 @@ Supported editor types: - dxTextArea - dxTextBox +### 7.1 Editor Script and Editor Options (builder-backed) + +`EditorScript` and `EditorOptions` are produced from a shared vocabulary, in two places that +must stay identical: + +| Surface | Location | +| --- | --- | +| Visual builder (UI) | `ui/src/views/admin/listForm/edit/json-row-operations/editor-script/scriptRecipes.ts` and `.../editor-options/optionSpecs.ts` + `presets.ts` | +| Seeder API (C#) | `api/src/Sozsoft.Platform.Domain.Shared/Editors/` — `EditorScript`, `EditorScriptRule`, `EditorScriptCondition`, `EditorOptions`, `EditorOptionsBuilder` | + +Rules AI must follow: + +- Prefer a recipe over hand-written JS. Recipes cover arithmetic (`Multiply`, `Subtract`, + `Percent`, `Sum`, `Formula`), dates (`Today`, `Days`, `Hours`), data movement (`Copy`, + `SetValue`, `Clear`, `ApiToField`), and view/notify actions (`ReadOnly`, `Notify`, `Ask`, + `OpenUrl`). Free-form JS is `Custom` and is the last option. +- Generated script carries a `// @builder {...}` header on the first line that stores the rules, + plus `// @runOnOpen` when any rule runs on form open. **Never edit the header by hand and never + emit a script that does not match what the builder would emit** — the dialog then treats the + script as manually edited and closes the rule editor. +- Rule triggers: `change` (default), `open` (default-value generation), `both`. Conditions join + with `and` / `or`. +- When a recipe changes on one side, change its port on the other side in the same task. +- In seeders write `EditorOptions = EditorOptions.Number(2).ShowClearButton()` style expressions + instead of raw JSON strings. Options marked `platform: true` in `optionSpecs.ts` deserialize + into typed DTOs (`GridBoxOptionsDto`, `TagBoxOptionsDto`, `ImageUploadOptionsDto`); wrong types + are silently ignored. + --- ## 8. Dynamic Form System @@ -172,6 +210,8 @@ Supported component families: - TreeList / Tree view - SubForm tabs (List, Tree, Gantt, Scheduler, Form, Chart) - Widget Group (dashboard KPI cards) +- TodoBoard — a Kanban board: columns come from `statusExpr` values, cards change status by drag + and drop, new columns (statuses) can be added from the board Runtime UI capabilities: @@ -212,6 +252,47 @@ If user requests custom code explicitly: - Provide a warning that low-code path is preferred. - Offer configuration-first alternative first. +### 10.1 Custom Component path (still not hand-written React) + +When a screen genuinely cannot be expressed as a ListForm, the next step is **not** a physical +React page. It is a Custom Component built in the Visual Designer and attached to a menu through +the Wizard's `Custom` path. + +- The component is stored in the database (`Name`, `RoutePath`, `Code`, `Props`, `Dependencies`, + `DataSources`) and compiled in the browser with `@babel/standalone`. +- The designer document lives in `Props.visualDesigner` **and** in the + `/*__SOZSOFT_VISUAL_DESIGNER__…__*/` header of the generated code. `sourceMode` is `visual` or + `code`; switching to `code` is one-way — the canvas cannot be restored afterwards, so do not + propose it casually. +- Toolbox families: `layout` (`PageContainer`, `FlexRow`, `Spacer`), `data` (`Form` — owns the + four CRUD endpoints and acts as a container), `platform` (`ListView`, `DataGridView`, + `TreeView`, `GanttView`, `TodoBoard`, `CardView`, `SchedulerView`, `PivotView`, `ChartView` — + each embeds a ListForm screen through `listFormCode`), `html`/`ui`, and other custom + components as `custom`. +- **Reuse before drawing:** if the requirement is a list/tree/chart that a ListForm already + serves, drop the matching `platform` node with its `listFormCode` instead of rebuilding it out + of primitives. +- Data binding goes through CRUD endpoints. Filter operators map to the CrudEndpoint `GetList` + contract: `eq` is a bare query parameter, every other operator carries its name as a suffix + (`?Name.contains=…`). +- Permissions are two-layered and mandatory: node-level `designerPermission` for visibility, and + per-command Form permissions (`selectPermission`, `insertPermission`, `updatePermission`, + `deletePermission`). In `Otomatik` mode the base is the read permission the Wizard created for + the component and the commands get `''` / `.Create` / `.Update` / `.Delete` suffixes. +- Manager screen permissions: `App.DeveloperKit.Components{,.Create,.Update,.Delete}`. +- Every save/delete writes `configs/seeds/{scope}/custom/{Name}.json`; the endpoints it uses are + referenced through `crud/{Entity}.json`. A component that is not seed-backed is not done. + +### 10.2 SQL View Designer + +For shaping data before a screen binds to it, prefer a designed view over an inline query: +`SqlViewDesignerDialog` produces T-SQL from a diagram (sources, joins, `CROSS/OUTER APPLY`, +derived subqueries) plus an SSMS-style criteria grid (Column / Alias / Output / Group By / Sort / +Filter / Or…). It is **one-way** — model to T-SQL. A view that does not parse back into the +canonical shape drops the dialog to raw SQL mode, so hand-editing a designed view costs the +designer. Aggregations available in the Group By column: `GroupBy`, `Where`, `SUM`, `COUNT`, +`COUNT_DISTINCT`, `AVG`, `MIN`, `MAX`. + --- ## 11. Integrations @@ -540,13 +621,32 @@ When in doubt, AI must choose the path that preserves: ## 31. Seeder-Driven Low-Code Development Guide (Authoritative) -AI must learn and teach implementation flow primarily from these seed assets: +There are **two seed surfaces**. AI must not mix them. -- api/src/Sozsoft.Platform.DbMigrator/Migrations/ListFormSeeder_Saas.cs -- api/src/Sozsoft.Platform.DbMigrator/Migrations/MenusData.json -- api/src/Sozsoft.Platform.DbMigrator/Migrations/PermissionsData.json -- api/src/Sozsoft.Platform.DbMigrator/Migrations/HostData.json -- api/src/Sozsoft.Platform.DbMigrator/Migrations/LanguagesData.json +**A. Repository seeds (shipped with code)** — `api/src/Sozsoft.Platform.DbMigrator/Migrations/`. +Platform-owned screens, menus, permissions and language texts. Changing them requires a build and +a deploy. AI must learn and teach implementation flow primarily from these assets: + +- ListFormSeeder_Saas.cs / ListFormSeeder_Administration.cs / ListFormSeeder_{Modul}.cs +- MenusData.json +- PermissionsData.json +- HostData.json +- LanguagesData.json +- WizardDataSeeder.cs / CustomComponentDataSeeder.cs / CrudDataSeeder.cs — the seeders that read + surface B below + +**B. Runtime seeds (written by the running application)** — `configs/seeds/`, resolved through +`SeedPathResolver`. The Wizard, Component Manager and CRUD Endpoint Manager write here; the +seeders above restore them when the database is recreated. Scope folders mirror the CDN layout: + +``` +configs/seeds/host/{wizard,custom,crud,sql/{object,execute},postgres/{object,execute}} +configs/seeds/tenants/{tenantId}/… same layout, tenant scope +configs/seeds/.imports/{importId}/… wizard import staging + backup (not scanned) +``` + +Rule: anything produced at runtime must land in surface B. **A feature that disappears when the +database is recreated is not delivered.** If user asks "how to add a new module/screen", AI must answer with this exact operational sequence. @@ -659,6 +759,27 @@ When user asks for a screen/module, AI must answer in this order: AI should produce practical, copy-adaptable artifact definitions and avoid abstract-only explanations. +### 31.9 Screen brief and image-based requests + +A request arrives either as prose or as a **screenshot / mockup / photo** of the desired screen. +Both enter the same pipeline: + +1. Turn it into a single screen brief — purpose, data source, columns and editors, layouts, + actions, sub-screens, widgets, approval flow, menu, permissions, tenancy, language texts. + For an image, read it first: page regions → main region type → toolbar → columns → filters → + form controls → row actions. +2. Pick the tool: grid-shaped work → SQL Query Manager + Wizard; free layout / dashboard / + single-record custom form → Custom Component. **When in doubt, pick the Wizard.** +3. Produce the seed files in dependency order. + +An image is a reference, not a pixel contract: build the closest thing the platform's own +components give, and state at the end what had no equivalent. Never block on missing detail — +state the assumption and continue; ask only where a wrong guess loses data or picks the wrong +approver. + +The concrete brief template, the image-element → artifact mapping and the visual-control → +`EditorType` mapping are in `lowcode.instructions.md` §0.6–§0.7. + --- ## 32. Mandatory Default Behaviors (Do Not Ask Repeatedly) @@ -701,7 +822,54 @@ Rules: - Permission contract must be created/bound for both parent and child menu items. - Route/menu consistency remains mandatory (`Url` and screen contract must match). -### 32.3 AI enforcement requirement +### 32.3 Wizard component kind and menu creation + +`ListFormWizardDto.ComponentKind` is the wizard's fork and the first field in the seed file. +Default is `List`, so older seed files that lack it keep working. + +| Kind | Produces | Menu URL | Child permissions | +| --- | --- | --- | --- | +| `List` | ListForm + ListFormFields + Route + Menu + Permissions + language keys | `/admin/list/{ListFormCode}` | `.Create`, `.Update`, `.Delete`, `.Export`, `.Import`, `.Note` | +| `Custom` | Menu + Permissions + language keys; the screen is the selected Custom Component | the component's `RoutePath` | `.Create`, `.Update`, `.Delete` only | + +`Export` / `Import` / `Note` exist only in the ListForm ecosystem. Never propose them on the +`Custom` path. + +`CreateMenu = false` (menu-less wizard): no menu record — and no parent menu record — is created; +ListForm, permissions and language keys are still produced. Use it when the screen is meant to be +embedded elsewhere as a SubGrid/part. Default is `true`. + +### 32.4 Permission group selection + +The wizard's permission group comes from a select list backed by `GetWizardPermissionGroups`. +Display names are read from the language key whose name equals the group name. + +- Selecting an existing group fills `PermissionGroupDisplayNameEn/Tr`; if left blank the server + writes the database values back, so the seed file always carries both languages. +- Defining a new group writes those texts to the language key with the same name as the group. +- AI must never propose a permission group without EN + TR display names. + +### 32.5 Wizard export/import is a package, not a file + +Exporting a wizard produces a zip whose paths are relative to the scope folder +(`host` / `tenants/{tenantId}`) and which carries every dependency: + +- `wizard/{file}.json` +- `custom/{component}.json` (with its dependencies) — `Custom` path +- `crud/{entity}.json` — endpoints those components use +- `{sql|postgres}/{object|execute}/{object}.sql` — the object behind `SelectCommand` on the + `List` path + +Import is two-phase: `AnalyzeImport` (unzip to `.imports/{id}/staged`, classify each entry as +`New` / `Identical` / `Conflict`, conflicts resolved in a diff editor) then `ApplyImport` +(file-by-file write, previous content backed up so `RollbackImport` can undo the whole batch), +closed by `CompleteImport`. Limits: allowed root folders `wizard`, `crud`, `custom`, `sql`, +`postgres` (and only `object` / `execute` under the SQL provider folders); 5 MB per entry, 50 MB +per archive, 500 entries. Permissions: `App.Listforms.Wizard.Export` / `.Import`. + +AI must not propose moving a screen between environments by copying a single json file. + +### 32.6 AI enforcement requirement AI must proactively apply these defaults in: diff --git a/.github/instructions/list.instructions.md b/.github/instructions/list.instructions.md index 0022381d..08d436e8 100644 --- a/.github/instructions/list.instructions.md +++ b/.github/instructions/list.instructions.md @@ -7,6 +7,10 @@ This document summarizes the rules, standards, and step-by-step instructions for --- +> **Bu doküman depo içi seeder'larla (DbMigrator/Migrations) modül eklemeyi anlatır.** +> Çalışma zamanında Wizard / Component Manager / CRUD Endpoint Manager ile üretim yapacaksan +> `lowcode.instructions.md` esastır; oradaki seed dosyaları `configs/seeds` altına düşer. + ## General Principles - **Configuration First:** Always prefer platform configuration (menus, permissions, forms, localization) over custom code. - **Modularization:** Each module (e.g., {Modul}) must have its own seeder, permission group, and localization entries. diff --git a/.github/instructions/lowcode.instructions.md b/.github/instructions/lowcode.instructions.md new file mode 100644 index 00000000..77071de7 --- /dev/null +++ b/.github/instructions/lowcode.instructions.md @@ -0,0 +1,2064 @@ +# Low-Code Üretim Referansı (Authoring Reference) + +Bu doküman, bir yapay zekâ ajanının **platformu bir geliştirici gibi kullanarak** ekran ve +bileşen üretebilmesi için gereken somut şemaları, alan anlamlarını, enum değerlerini ve +çalışan örnekleri içerir. + +- Ne yapılacağına karar verme kuralları: `ai.instructions.md` +- `api/` kod standardı: `dotnet.instructions.md` +- Modül/liste ekleme prosedürü: `list.instructions.md` +- Neyin nerede yaşadığı: `../../README.md` + +Çelişki olursa `ai.instructions.md` kazanır. Buradaki her şey **"kod yazmadan üretim"** +tarafındadır; bu dosya platformun çekirdek kodunu geliştirmek için değil, **prompt ile +uygulamaya yeni ekran, menü, yetki, dil anahtarı, route ve süreç eklemek** içindir. + +Bir istek geldiğinde çekirdek koda dokunmak değil, `configs/seeds/` altına uygulamanın kendi +üreteceği dosyaların aynılarını yazmak esastır — bkz. §0. + +> **Altın kural:** Ürettiğin her şey bir seed dosyasına düşmelidir. Veritabanı silinip +> yeniden oluşturulduğunda geri gelmeyen bir artefakt teslim edilmiş sayılmaz. + +--- + +## 0. Üretim protokolü (her prompt için zorunlu) + +Kullanıcı "şöyle bir ekran/form/süreç istiyorum" dediğinde **uygulamayı kullanıyormuş gibi** +davran: Wizard'ın, Component Manager'ın ve SQL Query Manager'ın diske yazacağı dosyaların +aynılarını `configs/seeds/host/` altına sen yaz. Ekranlara tıklamanın yerine geçen şey budur. + +Talep iki biçimde gelir ve ikisi de aynı hatta girer: + +- **Yazılı tarif** → §0.6 ile ekran tarifine çevir. +- **Ekran görüntüsü / mockup / fotoğraf** → §0.7 ile önce görseli oku, tarifi çıkar. + +Tarif hazır olunca §0.6.1 ile yolu seç (Wizard mı, Custom Component mi), sonra §0.2'deki sırayla +dosyaları üret. + +### 0.1 Yazılacak klasörler + +``` +configs/seeds/host/ +├── sql/object/{Nesne}.sql ← tablo / view / fonksiyon / prosedür (SQL Server) +├── sql/execute/{Ad}.sql ← bir kez çalışacak script (veri dolumu, migrasyon) +├── postgres/object|execute/… ← aynısının PostgreSQL diyalekti (hedef PG ise) +├── crud/{EntityName}.json ← tablonun REST uçları +├── custom/{ComponentName}.json ← Custom Component (Visual Designer dokümanı ya da kod) +└── wizard/{yyyyMMddHHmmss}_{WizardName}.json ← ekran + menü + yetki + dil anahtarları +``` + +Tenant kapsamı isteniyorsa `host/` yerine `tenants/{tenantId}/`; iç düzen aynıdır. + +### 0.2 Üretim sırası — bu sırayla yaz, bu sırayla çalışır + +| # | Adım | Dosya | Neden bu sırada | +| --- | --- | --- | --- | +| 1 | Tablo / view | `sql/object/…` | Ekranın bağlanacağı nesne önce var olmalı | +| 2 | CRUD uçları (gerekiyorsa) | `crud/{Entity}.json` | Custom Component'in data source'ları buna bakar | +| 3 | Custom Component (gerekiyorsa) | `custom/{Name}.json` | Wizard `Custom` yolunda bileşen adını arar | +| 4 | Wizard | `wizard/{ts}_{Ad}.json` | Menü, yetki, dil anahtarı ve ListForm'u üretir | +| 5 | Alt ekranlar (varsa) | ek `wizard/…` dosyaları | Ana wizard'dan **önceki** zaman damgasını alır | + +**Zaman damgası bir bağımlılık aracıdır.** `WizardDataSeeder` `wizard/` klasörünü **dosya adına +göre sıralı** işler. Bir wizard başka bir wizard'ın ürettiği menüye/ListForm'a dayanıyorsa, +bağımlı olanın zaman damgası **daha büyük** olmalıdır. Alt form olarak kullanılacak menüsüz +ekranı her zaman önce numaralandır. + +### 0.3 Yazmadan önce zorunlu keşif + +Uydurma. Şu üçünü mutlaka kontrol et: + +1. **Tablo/kolon adları** — `sql/object/` altındaki dosyalar ya da mevcut wizard dosyalarındaki + `SelectCommand`. Kolon adını tahmin etme; yoksa tabloyu da sen üret. +2. **Var olan menü kökü ve izin grubu** — aynı modül için ikinci bir kök menü açma. Mevcut + wizard dosyalarındaki `MenuParentCode` / `PermissionGroupName` değerlerini tara. +3. **Dil anahtarı çakışması** — `LanguagesData.json` ve diğer wizard dosyaları. Var olan bir + anahtarın metnini seed **ezmez**; yanlış metinle karşılaşırsan anahtarı değiştir. + +### 0.4 Üretim sonrası + +- Değişikliklerin devreye girmesi için **DB Migrate** (SQL Query Manager / Wizard Manager + üzerindeki buton, yetki `App.Setup.Migrate`) ya da migrator konteyneri (`SEED=true`) çalışmalı. +- Seeder **idempotenttir**: ekran zaten varsa dosya atlanır (§4.5). Var olan bir ekranı + değiştirmek için dosyayı düzenlemek yetmez — kayıtları silip yeniden seed etmek ya da + ekranı Wizard'dan `EditFileName` ile yeniden çalıştırmak gerekir. +- Ürettiğin dosyaları kullanıcıya **tek tek listele**: hangi dosya, ne üretiyor, hangi menüde + görünecek, hangi yetkiyi ister. + +### 0.5 Dosya biçimi kuralları + +| Kural | Değer | +| --- | --- | +| Kodlama | UTF-8 (BOM'suz) | +| JSON alan adları | **PascalCase** (`Wizard`, `ListFormCode`, `Groups`) — seeder `PropertyNameCaseInsensitive` okur ama dosyalar PascalCase üretilir | +| Custom component dosyası | Kök alanlar PascalCase; `Props` **içindeki** designer dokümanı camelCase (`visualDesigner`, `nodes`, `sourceMode`) | +| Enum'lar | Sayı olarak yazılır (§3) | +| `GeneratedAt` | ISO-8601 UTC (`2026-08-26T10:15:00Z`) — yalnızca bilgi amaçlı | +| İç içe JSON | `EditorOptions`, `EditorScript`, `LookupQuery` → JSON **string** olarak kaçışlanır | +| Satır sonu | LF | + +--- + +### 0.6 Talebi ekran tarifine çevir + +Dosya yazmadan önce **tek bir ekran tarifi** çıkar ve kullanıcıya kısaca doğrulat. Tarif, hangi +aracın kullanılacağını (Wizard mı, Custom Component mi) ve hangi dosyaların üretileceğini +belirler. + +**Ekran tarifi şablonu** — her maddeye bir satır, bilinmiyorsa varsayımını yaz: + +``` +Amaç : Ne işi görüyor, kim kullanıyor +Veri kaynağı : Tablo/view adı · yoksa "üretilecek" +Anahtar : Kolon + tip +Sütunlar : Alan → editör tipi → zorunlu mu → lookup kaynağı +Layout(lar) : Grid / Card / Pivot / Chart / Tree / Gantt / Scheduler / Todo +Form düzeni : Gruplar ve kolon sayısı +Aksiyonlar : Toolbar ve satır butonları, ne yapıyorlar +Alt ekranlar : Ana-detay sekmeleri +Widget'lar : Üstteki KPI kartları +Akış : Onay/koşul adımları +Menü : Üst menü, sıra, ikon +Yetki : Grup adı + kimde hangi yetki +Tenant : IsTenant / IsBranch / IsOrganizationUnit +Dil : EN + TR metinler +``` + +Kural: **eksik bilgi için işi durdurma.** Varsayımını tarifin içine yaz, üret, sonunda +"şu varsayımlarla ürettim" diye bildir. Yalnızca yanlış varsayımın veri kaybettireceği yerlerde +(hangi tabloya yazılacağı, kimin onaylayacağı) sor. + +#### 0.6.1 Hangi araç? — yol seçimi + +Tarifi çıkardıktan sonra tek bir karar kalır: + +| Tarifte varsa | Yol | +| --- | --- | +| Kayıt listesi + ekle/düzenle/sil + filtre/arama | **SQL Query Manager + Wizard** (`ComponentKind: 0`) | +| Aynı verinin birden çok görünümü (pano, takvim, ağaç, grafik) | **Wizard** — tek dosya, birden çok layout (§9.8) | +| Ana kayıt + detay sekmeleri | **Wizard** + `SubForms` | +| Onay süreci | **Wizard** + `WorkflowDto` | +| Üstte KPI kartları | **Wizard** + `Widgets` | +| Serbest yerleşim: yan yana kartlar, özel bölümler, sihirbaz adımları, karışık bileşenler | **Custom Component** (`ComponentKind: 1`) | +| Var olan ekranların bir sayfada toplanması (dashboard) | **Custom Component** + `platform` düğümleri (`listFormCode`) | +| Tek kayıt üzerinde çalışan özel form (liste yok) | **Custom Component** + `Form` düğümü | + +Şüphede kalırsan **Wizard'ı seç.** Grid'e benzeyen her şey Wizard'la daha ucuz, daha yetkin ve +daha bakımlıdır; Custom Component'e yalnızca yerleşim gerçekten grid/form kalıbına sığmıyorsa geç. + +İki yol birlikte de kullanılır: veriyi Wizard ile ekran yap, sonra Custom Component içine +`platform` düğümü olarak göm. + +--- + +### 0.7 Görselden üretim (ekran görüntüsü / mockup) + +Kullanıcı tarif yerine **bir fotoğraf, ekran görüntüsü ya da mockup** gönderdiğinde önce görseli +okuyup §0.6'daki tarifi çıkar, sonra dosyaları üret. Görseli tahmine değil, gördüğün öğelere +dayandır. + +#### 0.7.1 Görseli okuma sırası + +1. **Sayfa iskeleti** — kaç bölge var? Üstte KPI şeridi, ortada liste, sağda panel, altta sekme? +2. **Ana bölge ne?** Tablo mu, kart ızgarası mı, pano mu, takvim mi, form mu? +3. **Araç çubuğu** — hangi butonlar var, ne yazıyorlar, ikonları ne? +4. **Sütunlar** — başlıklar, hizalama, biçim (para, tarih, yüzde), rozet/renkli durum hücreleri. +5. **Filtre/arama** — başlık altı filtre satırı, arama kutusu, tarih aralığı seçicileri. +6. **Form alanları** — kontrol tipleri ve kolon düzeni (kaç kolon, hangi alan kaç sütun kaplıyor). +7. **Satır aksiyonları** — satır sonundaki ikonlar. +8. **Durum göstergeleri** — onay adımları, ilerleme çubukları, etiketler. + +#### 0.7.2 Görsel öğe → artefakt eşlemesi + +| Görselde gördüğün | Karşılığı | +| --- | --- | +| Başlık satırı + veri satırları olan tablo | Grid layout | +| Kart ızgarası | Card layout | +| Dikey kolonlar + sürüklenebilir kartlar | Todo (Kanban) layout · `TodoOptionDto.StatusExpr` | +| Ay/hafta takvimi, saat çizelgesi | Scheduler layout | +| Yatay zaman çubukları, bağımlılık okları | Gantt layout | +| Girintili ağaç, açılır düğümler | Tree layout | +| Satır/sütun kesişimli özet tablo, ara toplamlar | Pivot layout | +| Çubuk/çizgi/pasta grafik | Chart layout · `SeriesJson` | +| Üstte sayı gösteren küçük kutular | `Widgets` (`WidgetEditDto`) | +| Kaydın altında sekmeler (Kalemler, Ekler, Geçmiş) | `SubForms` | +| Araç çubuğundaki özel buton | `CommandColumnJson` · `ButtonPosition: 1` | +| Satır sonundaki ikon | `CommandColumnJson` · `ButtonPosition: 0` | +| Başlık altındaki filtre satırı | `FilterRowJson` | +| Sağ üstteki arama kutusu | `SearchPanelJson` | +| "Sürükleyip gruplayın" şeridi | `GroupPanelJson` | +| Üstte ek filtre çubuğu (tarih aralığı, şube seçimi) | `ExtraFilterJson` | +| Sayfalama şeridi | `PagerOptionJson` | +| Renkli durum rozeti | `ColumnStylingJson` / `ColumnCssClass` | +| Onayla / Reddet butonları | `WorkflowJson` (otomatik gelir, elle tanımlama) | +| Yan yana bağımsız bölümler, karışık yerleşim | Custom Component · `FlexRow` + düğümler | + +#### 0.7.3 Görseldeki kontrol → `EditorType` + +| Görselde | `EditorType` | Ek | +| --- | --- | --- | +| Tek satır metin kutusu | `dxTextBox` | | +| Çok satırlı kutu | `dxTextArea` | `autoResizeEnabled` | +| Sağında ok olan açılır liste | `dxSelectBox` | lookup zorunlu | +| İçinde etiket/çip olan açılır liste | `dxTagBox` | lookup zorunlu | +| Büyüteçli seçim + tablo açılıyor | `dxGridBox` / `dxLookup` | `columns` | +| Onay kutusu | `dxCheckBox` | | +| Aç/kapa anahtarı | `dxSwitch` | `switchedOnText` | +| Yan yana yuvarlak seçenekler | `dxRadioGroup` | `layout: horizontal` | +| Takvim ikonlu alan | `dxDateBox` | | +| İki tarihli aralık alanı | `dxDateRangeBox` | | +| Artı/eksi oklu sayı | `dxNumberBox` | `format.precision` | +| `₺ 1.250,00` biçimli alan | `dxNumberBox` | `format.type: currency` | +| `%` biçimli alan | `dxNumberBox` | `format.type: percent` | +| Renk kutucuğu | `dxColorBox` | | +| Kalın/italik araç çubuklu alan | `dxHtmlEditor` | `toolbar.items` | +| Kaydırıcı | `dxSlider` / `dxRangeSlider` | `min`, `max` | +| Görsel küçük resmi / yükleme alanı | `dxImageViewer` / `dxImageUpload` | `accept`, `maxFileSize` | +| Gri, tıklanamaz alan | ilgili editör + `EditorOptions: {"readOnly": true}` | | +| Kırmızı yıldızlı etiket | `IsRequired: true` | | + +#### 0.7.4 Görselden okunamayacak şeyler + +Bunlar bir ekran görüntüsünde **görünmez**; varsayımını yaz ve bildir: + +| Bilgi | Varsayılan davranış | +| --- | --- | +| Tablo/kolon adları | Görünen başlıklardan PascalCase kolon adı türet, tabloyu `sql/object/` altında sen üret | +| Anahtar alan ve tipi | `Id` / `UNIQUEIDENTIFIER` (`KeyFieldDbSourceType: 9`) | +| Lookup'ların kaynağı | Az sayıda sabit seçenek görünüyorsa `StaticData`, kod/isim çifti gerektiren yerlerde `Query` | +| Tenant/şube kırılımı | `IsTenant: true` (§CLAUDE varsayılanı) | +| Yetki grubu ve menü kökü | Ekranın ait olduğu modülden türet, kullanıcıya sor değil bildir | +| Dil metinleri | Görseldeki dilde yaz, diğer dile çevir; ikisi de doldurulur | +| Onaycılar | Yer tutucu bırak (``) ve kullanıcıya sor — **bu, sorulması gereken bir şeydir** | +| İş kuralları (hesap, koşul) | Görselde formül görünmüyorsa `EditorScript` yazma; sor | + +#### 0.7.5 Görsel için de aynı protokol + +Görselden çıkardığın tarifi §0.6.1'deki tabloya sok, yolu seç, §0.2'deki sırayla dosyaları üret. +Görsel bir "tasarım sözleşmesi" değildir: platformun kendi bileşenleriyle **en yakın** karşılığı +üretilir, piksel taklidi yapılmaz. Görseldeki bir öğenin platformda karşılığı yoksa bunu üretim +sonunda açıkça yaz. + +--- + +## 1. Hangi istek → hangi artefakt + +| İstek | Üretilecek artefakt | Bölüm | +| --- | --- | --- | +| "X tablosunun listesi/ekranı olsun" | Wizard seed dosyası (`ComponentKind: 0`) | 4 | +| "Şu alanı otomatik hesaplasın / şarta göre kilitlensin" | `EditorScript` (Script Builder tarifi) | 5 | +| "Alan şu formatta / maskeyle görünsün" | `EditorOptions` (Options Builder) | 6 | +| "Ekran bir tablodan değil, birleştirilmiş veriden beslensin" | SQL View Designer → `View` + Wizard | 8 | +| "Serbest yerleşimli bir sayfa / dashboard / özel form" | Custom Component + Wizard (`ComponentKind: 1`) | 7 | +| "Bir tabloya REST API açalım" | CRUD Endpoint | 9 | +| "Elle yazdığım bir SQL'i API olarak açalım" | Custom Endpoint | 9.2 | +| "API'de iş mantığı/hesap/entegrasyon lazım" | Dynamic Service | 9.3 | +| "Onay süreci olsun" | `WorkflowDto` + workflow kriterleri | 4.7 | +| "Ana-detay ekranı" | `SubForms` | 4.6 | +| "Üstte KPI kartları" | `Widgets` | 4.6 | +| Hiçbiri yetmiyorsa | Dynamic Service → en son çare kod | `ai.instructions.md` §4 | + +--- + +## 2. İsimlendirme sözleşmesi + +Tutarlılık zorunludur; menü, route, yetki ve dil anahtarı aynı kökten türer. + +| Şey | Kalıp | Örnek | +| --- | --- | --- | +| Tablo | `{Modul}_T_{Entity}` | `Mrp_T_Order` | +| View | `{Modul}_V_{Ad}` | `Mrp_V_OrderSummary` | +| `ListFormCode` | `App.{Modul}.{Liste}` | `App.Mrp.Orders` | +| `MenuCode` | `ListFormCode` ile aynı | `App.Mrp.Orders` | +| Üst menü kodu | `App.{Modul}` | `App.Mrp` | +| Yetki grubu | `App.{Modul}` | `App.Mrp` | +| Yetkiler | `{MenuCode}` + `.Create/.Update/.Delete/.Export/.Import/.Note` | `App.Mrp.Orders.Create` | +| Dil anahtarı (menü/başlık) | `{MenuCode}` | `App.Mrp.Orders` | +| Dil anahtarı (alan) | `App.Listform.ListformField.{Alan}` | `App.Listform.ListformField.OrderNo` | +| Route (List) | `/admin/list/{ListFormCode}` — statik `:listFormCode` route'u karşılar, DB'ye kayıt yazılmaz | `/admin/list/App.Mrp.Orders` | +| Route (Custom) | Bileşenin `RoutePath` değeri; route doğrudan bundan üretilir | `/admin/order-board` | +| Wizard seed dosyası | `{yyyyMMddHHmmss}_{WizardName}.json` | `20260826101500_Orders.json` | +| Custom component seed | `{Name}.json` | `OrderBoard.json` | +| CRUD endpoint seed | `{EntityName}.json` | `Mrp_T_Order.json` | + +Kapsam klasörü: host bağlamında `configs/seeds/host/…`, tenant bağlamında +`configs/seeds/tenants/{tenantId}/…`. Çözüm her zaman `SeedPathResolver` üzerinden yapılır; +yol elle kurulmaz. + +--- + +## 3. Ortak enum değerleri (seed dosyalarına **sayı** olarak yazılır) + +**`ComponentKind`** — `0` List · `1` Custom + +**`SelectCommandType`** — `1` Table · `2` View · `3` TableValuedFunction · `4` Query · +`5` StoredProcedure + +**`LookupDataSourceType`** — `1` StaticData · `2` Query · `3` WebService + +**`DbSourceType` / `KeyFieldDbSourceType`** (`System.Data.DbType`): + +| # | Ad | SQL karşılığı | +| --- | --- | --- | +| 1 | Binary | `binary`, `varbinary`, `image` | +| 2 | Byte | `tinyint` | +| 3 | Boolean | `bit` | +| 5 | Date | `date` | +| 6 | DateTime | `datetime`, `datetime2`, `smalldatetime` | +| 7 | Decimal | `decimal`, `numeric`, `money` | +| 8 | Double | `float`, `real` | +| 9 | Guid | `uniqueidentifier` | +| 10 | Int16 | `smallint` | +| 11 | Int32 | `int` | +| 12 | Int64 | `bigint` | +| 16 | String | `nvarchar`, `varchar`, `nchar`, `char`, `text` | +| 17 | Time | `time` | +| 25 | Xml | `xml` | +| 27 | DateTimeOffset | `datetimeoffset` | + +**Editör tipleri (`EditorType`)** — 21 tip. DevExtreme'den gelenler: `dxAutocomplete`, +`dxCalendar`, `dxCheckBox`, `dxColorBox`, `dxDateBox`, `dxDateRangeBox`, `dxDropDownBox`, +`dxHtmlEditor`, `dxLookup`, `dxNumberBox`, `dxRadioGroup`, `dxRangeSlider`, `dxSelectBox`, +`dxSlider`, `dxSwitch`, `dxTagBox`, `dxTextArea`, `dxTextBox`. Platformun kendi editörleri +(`PlatformEditorTypes`): `dxGridBox`, `dxImageViewer`, `dxImageUpload` (+ `dxTagBox` platform +tarafında da özel işlenir). + +Tip seçimi kuralı: `Boolean → dxCheckBox`/`dxSwitch`, tarih → `dxDateBox`, sayı → `dxNumberBox`, +FK/lookup → `dxSelectBox` (az kayıt) veya `dxGridBox`/`dxLookup` (çok kayıt/çok kolon), +çok değerli FK → `dxTagBox`, uzun metin → `dxTextArea`, HTML → `dxHtmlEditor`. +Her editörün kendine özgü seçenekleri için §4.3.2. + +--- + +## 4. Artefakt A — ListForm ekranı (Wizard seed dosyası) + +Konum: `configs/seeds/{kapsam}/wizard/{yyyyMMddHHmmss}_{WizardName}.json` +Okuyan: `WizardDataSeeder` · Yazan: `ListFormWizardAppService` + +### 4.1 Dosya iskeleti + +```jsonc +{ + "Wizard": { /* ListFormWizardDto — 4.2 */ }, + "IsDeletedField": true, // tabloda IsDeleted var mı (soft delete) + "IsCreatedField": true, // tabloda CreatorId var mı (audit) + "InsertedRecords": { // bu çalıştırmanın GERÇEKTEN yarattığı kayıtlar + "LanguageKeys": [], "PermissionGroupNames": [], "PermissionNames": [], + "MenuCodes": [], "DataSourceCodes": [] + } +} +``` + +`InsertedRecords` **silme sözleşmesidir**: wizard silindiğinde yalnızca burada listelenen +kayıtlar silinir, paylaşılan kayıtlara (var olan izin grubu, var olan üst menü) dokunulmaz. +Elle dosya yazarken buraya var olan bir kaydı koyma — silme onu da götürür. + +### 4.2 `Wizard` bloğu — alan alan + +**Kimlik ve yol** + +| Alan | Anlam | +| --- | --- | +| `ComponentKind` | `0` List (varsayılan) · `1` Custom. Dosyadaki ilk alan olmalı. | +| `WizardName` | Dosya adının ve export zip adının kaynağı. | +| `ListFormCode` | Ekranın kodu; route ve metadata bu koda göre çözülür. | +| `MenuCode` | Menü kaydının kodu; yetki adlarının kökü de budur. | +| `MenuUrl` | List'te `/admin/list/{MenuCode}` hesaplanır; Custom'da bileşenin `RoutePath`'i yazılır. | +| `CustomComponentName` | Yalnızca Custom yolunda; bağlanacak bileşenin adı. | +| `EditFileName` | Yalnızca **istek girdisi**: doluysa güncellemedir, sunucu önce eski dosyayı ve ürettiği kayıtları siler. Seed dosyasına yazılmaz. | + +**Menü** + +| Alan | Anlam | +| --- | --- | +| `CreateMenu` | `false` ise menü ve üst menü kaydı **hiç üretilmez**; ListForm, yetki ve dil anahtarları yine üretilir. SubGrid/parça olarak kullanılacak ekranlar için. Varsayılan `true`. | +| `MenuParentCode` / `MenuParentShortName` / `MenuParentIcon` | Üst menü; yoksa oluşturulur. | +| `MenuIcon` | React-icons adı (`FcBiohazard`, `FaBox`…). | +| `MenuOrder` | Kardeşler arası sıra. | + +**Yetki ve dil** + +| Alan | Anlam | +| --- | --- | +| `PermissionGroupName` | Var olan grup ya da yeni grup adı. | +| `PermissionGroupDisplayNameEn` / `Tr` | Grup adıyla aynı dil anahtarına yazılır. Var olan grup seçilip boş bırakılırsa sunucu veritabanındaki değeri geri yazar. | +| `LanguageTextMenuEn` / `Tr` | Menü etiketi. | +| `LanguageTextTitleEn` / `Tr` | Ekran başlığı (yalnızca List yolunda üretilir). | +| `LanguageTextDescEn` / `Tr` | Ekran açıklaması (yalnızca List yolunda). | +| `LanguageTextMenuParentEn` / `Tr` | Üst menü etiketi. | + +Üretilen yetkiler: `{MenuCode}` (okuma, kök) + List yolunda `.Create .Update .Delete .Export +.Import .Note`, Custom yolunda yalnızca `.Create .Update .Delete`. Hepsi admin rolüne grant +edilir. + +**Veri** + +| Alan | Anlam | +| --- | --- | +| `DataSourceCode` | Bağlantı kodu; varsayılan `"Default"`. | +| `DataSourceConnectionString` | Yeni bir bağlantı tanımlanıyorsa; aksi hâlde boş. **Asla gerçek kimlik bilgisi yazma.** | +| `SelectCommandType` | Bkz. §3. | +| `SelectCommand` | Tablo/view adı, fonksiyon çağrısı ya da sorgu metni. | +| `KeyFieldName` / `KeyFieldDbSourceType` | Birincil anahtar ve tipi. | + +**Davranış** + +| Alan | Anlam | +| --- | --- | +| `IsTenant` / `IsBranch` / `IsOrganizationUnit` | Otomatik kırılım filtresi. Tabloda `TenantId` varsa `IsTenant: true` **olmalı**. | +| `AllowAdding` / `AllowUpdating` / `AllowDeleting` / `AllowDetail` / `ConfirmDelete` | Toolbar ve satır aksiyonları. | +| `DefaultLayout` | `"grid"`, `"card"`, `"pivot"`, `"chart"`, `"tree"`, `"gantt"`, `"scheduler"`, `"todo"`. | +| `Grid` `Card` `Pivot` `Chart` `Tree` `Gantt` `Scheduler` `Todo` | Hangi görünümlerin açık olduğu. Açtığın her görünümün option bloğunu da doldur. | + +**Görünüm option blokları** — yalnızca ilgili bayrak `true` ise anlamlıdır: + +| Blok | Zorunlu alanlar | +| --- | --- | +| `TreeOptionDto` | `KeyExpr`, `ParentIdExpr`; opsiyonel `HasItemsExpr`, `RootValue`, `AutoExpandAll`, `RecursiveSelection` | +| `GanttOptionDto` | `KeyExpr`, `ParentIdExpr`, `TitleExpr`, `StartExpr`, `EndExpr`; `ProgressExpr`, `ScaleType` (`hours`/`days`/`weeks`/`months`), `Allow*` bayrakları | +| `SchedulerOptionDto` | `TextExpr`, `StartDateExpr`, `EndDateExpr`; `AllDayExpr`, `RecurrenceRuleExpr`, `StartDayHour`, `EndDayHour`, `DefaultView` (`day`/`week`/`month`), `CellDuration`, `FirstDayOfWeek`, `Allow*` | +| `TodoOptionDto` | `TitleExpr`, `StatusExpr`; `DescriptionExpr`, `DueDateExpr`, `TagExpr`, `AssigneeExpr`, `PriorityExpr`, `CompletedExpr`, `OrderExpr`, `StatusOrder` (virgülle ayrılmış kolon sırası), `AllowDragging` | + +### 4.3 `Groups` — alanlar ve düzenleme formu + +`Groups`, hem grid sütunlarını hem düzenleme formunun düzenini tanımlar. Buradaki her `Items` +öğesi bire bir bir **`ListFormField` kaydına** dönüşür (§4.8); ekranda görünen sütunların tek +kaynağı odur. Grup yapısı yalnızca düzenleme formunun yerleşimini belirler — grid sütun sırası +`Items` sırasından gelir. + +```jsonc +"Groups": [ + { + "Caption": "Genel", + "ColCount": 2, + "Items": [ /* WizardColumnItemInputDto */ ] + } +] +``` + +Her `Items` öğesi: + +| Alan | Anlam | +| --- | --- | +| `FieldName` | Veri kolonunun adı (SQL'deki adıyla birebir). | +| `CaptionName` | Dil anahtarı ya da düz başlık. | +| `TurkishCaption` / `EnglishCaption` | Dil metinleri; ikisi de doldurulur. | +| `EditorType` | Bkz. §3. | +| `EditorOptions` | DevExtreme editör JSON'u — §6. | +| `EditorScript` | Alan davranış script'i — §5. | +| `DbSourceType` | Bkz. §3. | +| `IsRequired` | Zorunluluk. | +| `IncludeInEditingForm` | `false` ise yalnızca grid sütunu olur, forma girmez. | +| `ColSpan` | Formda kaç kolon kaplar (grubun `ColCount` değeri içinde). | +| `LookupDataSourceType` | `1/2/3` — bkz. §3. `0` = lookup yok. | +| `LookupQuery` | `Query` tipinde SQL; `StaticData` tipinde JSON dizisi; `WebService` tipinde URL. | +| `ValueExpr` / `DisplayExpr` | Lookup'ın değer ve etiket kolonları. | + +### 4.3.1 Sütunlar nasıl otomatik üretilir + +Gösterilecek **tüm sütunlar `ListFormField` kayıtlarında** durur. Wizard bu kayıtları veritabanı +kolon metadata'sından (`GetTableColumnsAsync`) otomatik üretir; sen yalnızca farklı olmasını +istediğin şeyi belirtirsin. + +Bir kolon `Groups[].Items[]` içine eklendiğinde varsayılanlar şöyle türetilir: + +| Alan | Nereden gelir | +| --- | --- | +| `EditorType` | SQL tipinden çıkarılır (aşağıdaki tablo) | +| `DbSourceType` | SQL tipinden `System.Data.DbType` karşılığına eşlenir (§3) | +| `IsRequired` | Kolon `NOT NULL` ise `true` | +| `TurkishCaption` / `EnglishCaption` | Kolon adı PascalCase'ten kelimelere ayrılır: `OrderNo` → `Order No` | +| `CaptionName` | `App.Listform.ListformField.{KolonAdı}` | +| `ColSpan` | `1` | +| `IncludeInEditingForm` | `true` | +| `ValueExpr` / `DisplayExpr` | `Key` / `Name` (lookup tanımlanırsa değiştirilir) | +| `EditorOptions` / `EditorScript` | Boş — ihtiyaç varsa sen yazarsın | + +**SQL tipi → `EditorType` çıkarımı** (`inferEditorType`): + +| SQL tipi | Üretilen editör | +| --- | --- | +| `bit` | `dxCheckBox` | +| `int`, `bigint`, `smallint`, `tinyint`, `decimal`, `numeric`, `float`, `real`, `money`, `smallmoney` | `dxNumberBox` | +| `date`, `datetime`, `datetime2`, `smalldatetime`, `datetimeoffset` | `dxDateBox` | +| diğer hepsi | `dxTextBox` | + +Çıkarım kasıtlı olarak dardır. **Foreign key kolonları `dxNumberBox`/`dxTextBox` olarak gelir** — +bunları `dxSelectBox` / `dxGridBox` / `dxTagBox` yapmak ve lookup tanımlamak senin işindir. +Uzun metin (`nvarchar(max)`) için `dxTextArea`, HTML içerik için `dxHtmlEditor`, durum kolonları +için `dxRadioGroup`/`dxSwitch` seçilmelidir; otomatik çıkarım bunları bilemez. + +### 4.3.2 Editör tipine göre özellikler + +Her editör kendi `EditorOptions` sözlüğüne sahiptir. Aşağıdaki tablo, o editöre **özgü** olan +anahtarları listeler; ortak anahtarlar (§4.3.3) her editörde geçerlidir. + +| `EditorType` | Ne için | Uygun `DbSourceType` | Kendine özgü `EditorOptions` | +| --- | --- | --- | --- | +| `dxTextBox` | Kısa metin | String (16) | — (ortak: `maxLength`, `mask`, `placeholder`) | +| `dxTextArea` | Uzun metin | String (16) | `autoResizeEnabled`, `minHeight` | +| `dxNumberBox` | Sayı | Int32/Int64/Decimal/Double (7,8,10,11,12) | `format.type` (`fixedPoint`/`currency`/`percent`), `format.precision`, `format.currency`, `useMaskBehavior`, `useLargeSpinButtons`, `invalidValueMessage` | +| `dxDateBox` | Tarih / tarih-saat | Date/DateTime (5,6,26,27) | `useMaskBehavior` (+ ortak: `type`, `displayFormat`, `dateSerializationFormat`, `pickerType`, `interval`, `calendarOptions.*`, `invalidDateMessage`) | +| `dxDateRangeBox` | Tarih aralığı | Date/DateTime | ortak tarih anahtarları | +| `dxCalendar` | Gömülü takvim | Date/DateTime | `calendarOptions.zoomLevel`, `calendarOptions.firstDayOfWeek` | +| `dxCheckBox` | Evet/hayır | Boolean (3) | `text` (kutunun yanındaki etiket) | +| `dxSwitch` | Evet/hayır (anahtar) | Boolean (3) | `switchedOnText`, `switchedOffText` | +| `dxRadioGroup` | Az sayıda seçenek | String/Int32 | `layout` (`horizontal`/`vertical`) + **lookup zorunlu** | +| `dxSelectBox` | Tek seçim (az kayıt) | FK tipi | `acceptCustomValue`, `searchEnabled`, `minSearchLength`, `searchExpr`, `searchMode`, `searchTimeout`, `noDataText`, `showDataBeforeSearch` + **lookup zorunlu** | +| `dxLookup` | Tek seçim (çok kayıt, mobil dostu) | FK tipi | `dropDownOptions.*`, `searchEnabled`, `applyValueMode` + **lookup zorunlu** | +| `dxDropDownBox` | Özel içerikli açılır kutu | FK tipi | `dropDownOptions.width/height/hideOnOutsideClick`, `deferRendering`, `openOnFieldClick` | +| `dxGridBox` | Çok kolonlu seçim ızgarası | FK tipi | `columns`\*, `filterRowVisible`\*, `selectionMode`\*, `acceptCustomValue`\* + **lookup zorunlu** | +| `dxTagBox` | Çok değerli seçim | String (virgüllü) / ilişki tablosu | `showSelectionControls`\*, `maxDisplayedTags`\*, `showMultiTagOnly`\*, `applyValueMode`\*, `searchEnabled`\*, `acceptCustomValue`\*, `hideSelectedItems`, `multiline` + **lookup zorunlu** | +| `dxAutocomplete` | Serbest metin + öneri | String (16) | `minSearchLength`, `searchExpr`, `searchTimeout` | +| `dxColorBox` | Renk | String (16) | `editAlphaChannel`, `keyStep` | +| `dxSlider` | Tek değerli kaydırıcı | Sayı | `min`, `max`, `tooltip.showMode` | +| `dxRangeSlider` | Aralık kaydırıcı | Sayı | `min`, `max`, `showRange` | +| `dxHtmlEditor` | Zengin metin | String (16) | `toolbar.items`, `toolbar.multiline`, `valueType` (`html`/`markdown`), `mediaResizing.enabled`, `imageUpload.uploadUrl`, `imageUpload.fileUploadMode` | +| `dxImageUpload` | Görsel yükleme (platform) | String (16) | `uploadUrl`\*, `accept`\*, `multiple`\*, `maxFileSize`\*, `width`\*, `height`\* | +| `dxImageViewer` | Görsel gösterimi (platform) | String (16) | `width`, `height` | + +`\*` ile işaretli anahtarlar backend'de **tipli DTO'ya** çözülür: +`GridBoxOptionsDto`, `TagBoxOptionsDto`, `ImageUploadOptionsDto`. Yanlış tipte yazılan bir değer +hata vermez, **sessizce yok sayılır** — bu yüzden `columns` bir dizi, `maxDisplayedTags` bir +sayı, `multiple` bir boolean olmalıdır. + +**Lookup zorunlu** yazan editörlerde `LookupDataSourceType` + `LookupQuery` + `ValueExpr` + +`DisplayExpr` dolu olmalıdır; aksi hâlde alan boş bir liste gösterir: + +```jsonc +{ + "FieldName": "StatusId", "EditorType": "dxSelectBox", "DbSourceType": 11, + "LookupDataSourceType": 1, + "LookupQuery": "[{\"Key\":1,\"Name\":\"Taslak\"},{\"Key\":2,\"Name\":\"Onayda\"},{\"Key\":3,\"Name\":\"Onaylandı\"}]", + "ValueExpr": "Key", "DisplayExpr": "Name" +} +``` + +`LookupDataSourceType` `1` (StaticData) ise `LookupQuery` bir JSON dizisi, `2` (Query) ise SQL, +`3` (WebService) ise URL'dir. Cascade (ebeveyn–çocuk) davranışı ListForm editöründeki lookup +ayarlarından tanımlanır. + +### 4.3.3 Her editörde geçerli ortak seçenekler + +Gruplar hâlinde (`optionSpecs.ts` sözlüğünün tamamı UI'yı buradan üretir): + +| Grup | Anahtarlar | +| --- | --- | +| Genel | `disabled`, `readOnly`, `visible`, `hint`, `tabIndex`, `showClearButton`, `valueChangeEvent`, `validationMessageMode`, `validationMessagePosition` | +| Görünüm | `width`, `height`, `stylingMode`, `label`, `labelMode`, `elementAttr.class`, `inputAttr.style`, `inputAttr.aria-label`, `buttons` | +| Metin | `placeholder`, `maxLength`, `spellcheck`, `mode`, `mask`, `maskChar`, `maskRules.X`, `maskInvalidMessage`, `showMaskMode`, `useMaskedValue`, `encodeHtml` | +| Sayı | `min`, `max`, `format`, `showSpinButtons` | +| Tarih | `type`, `displayFormat`, `dateSerializationFormat`, `pickerType`, `interval`, `invalidDateMessage` | +| Açılır liste | `noDataText`, `searchEnabled`, `minSearchLength`, `searchExpr`, `searchMode`, `searchTimeout`, `showDataBeforeSearch`, `openOnFieldClick`, `deferRendering`, `wrapItemText`, `dropDownOptions.*` | + +Elle JSON yazmak yerine C# tarafında `EditorOptions` akıcı yapısını kullan (§6); hazır +başlangıçlar bu anahtarların doğru bileşimlerini üretir. + +### 4.4 Çalışan örnek — tablo üzerinden liste ekranı + +```jsonc +{ + "Wizard": { + "ComponentKind": 0, + "WizardName": "Orders", + "ListFormCode": "App.Mrp.Orders", + "MenuCode": "App.Mrp.Orders", + "MenuOrder": 1, + "CreateMenu": true, + "MenuUrl": "/admin/list/App.Mrp.Orders", + "IsTenant": true, "IsBranch": false, "IsOrganizationUnit": false, + "AllowAdding": true, "AllowUpdating": true, "AllowDeleting": true, + "AllowDetail": false, "ConfirmDelete": true, + "DefaultLayout": "grid", + "Grid": true, "Card": true, "Pivot": true, "Chart": true, + "Tree": false, "Gantt": false, "Scheduler": false, "Todo": false, + + "LanguageTextMenuEn": "Orders", "LanguageTextMenuTr": "Siparişler", + "LanguageTextTitleEn": "Orders", "LanguageTextTitleTr": "Siparişler", + "LanguageTextDescEn": "Order list", "LanguageTextDescTr": "Sipariş listesi", + "LanguageTextMenuParentEn": "MRP", "LanguageTextMenuParentTr": "MRP", + + "PermissionGroupName": "App.Mrp", + "PermissionGroupDisplayNameEn": "MRP", "PermissionGroupDisplayNameTr": "MRP", + "MenuParentCode": "App.Mrp", "MenuParentShortName": "MRP", "MenuParentIcon": "FcFactory", + "MenuIcon": "FcShipped", + + "DataSourceCode": "Default", "DataSourceConnectionString": "", + "SelectCommandType": 1, + "SelectCommand": "Mrp_T_Order", + "KeyFieldName": "Id", + "KeyFieldDbSourceType": 11, + + "Groups": [ + { + "Caption": "Genel", "ColCount": 2, + "Items": [ + { + "FieldName": "OrderNo", "CaptionName": "App.Listform.ListformField.OrderNo", + "TurkishCaption": "Sipariş No", "EnglishCaption": "Order No", + "EditorType": "dxTextBox", "DbSourceType": 16, + "IsRequired": true, "IncludeInEditingForm": true, "ColSpan": 1, + "EditorOptions": "{\"showClearButton\":true,\"maxLength\":20}" + }, + { + "FieldName": "CustomerId", "CaptionName": "App.Listform.ListformField.Customer", + "TurkishCaption": "Cari", "EnglishCaption": "Customer", + "EditorType": "dxSelectBox", "DbSourceType": 11, + "IsRequired": true, "IncludeInEditingForm": true, "ColSpan": 1, + "LookupDataSourceType": 2, + "LookupQuery": "SELECT Id, Name FROM Crm_T_Customer WHERE IsDeleted = 0", + "ValueExpr": "Id", "DisplayExpr": "Name" + }, + { + "FieldName": "Quantity", "CaptionName": "App.Listform.ListformField.Quantity", + "TurkishCaption": "Miktar", "EnglishCaption": "Quantity", + "EditorType": "dxNumberBox", "DbSourceType": 7, + "IsRequired": true, "IncludeInEditingForm": true, "ColSpan": 1, + "EditorOptions": "{\"format\":{\"type\":\"fixedPoint\",\"precision\":2},\"showSpinButtons\":true}" + }, + { + "FieldName": "UnitPrice", "CaptionName": "App.Listform.ListformField.UnitPrice", + "TurkishCaption": "Birim Fiyat", "EnglishCaption": "Unit Price", + "EditorType": "dxNumberBox", "DbSourceType": 7, + "IsRequired": true, "IncludeInEditingForm": true, "ColSpan": 1, + "EditorOptions": "{\"format\":{\"type\":\"fixedPoint\",\"precision\":2}}" + }, + { + "FieldName": "Total", "CaptionName": "App.Listform.ListformField.Total", + "TurkishCaption": "Tutar", "EnglishCaption": "Total", + "EditorType": "dxNumberBox", "DbSourceType": 7, + "IsRequired": false, "IncludeInEditingForm": true, "ColSpan": 2, + "EditorOptions": "{\"readOnly\":true,\"format\":{\"type\":\"fixedPoint\",\"precision\":2}}", + "EditorScript": "// @builder {\"v\":1,\"rules\":[{\"id\":\"r1\",\"recipe\":\"multiply\",\"condition\":{\"operator\":\"always\",\"source\":\"\",\"value\":\"\"},\"params\":{\"left\":\"Quantity\",\"right\":\"UnitPrice\",\"target\":\"Total\",\"digits\":\"2\"}}]}\nset('Total', round(num('Quantity') * num('UnitPrice'), 2))" + } + ] + } + ], + "SubForms": [], "Widgets": [], + "WorkflowDto": { "ApprovalUserFieldName": "", "ApprovalDateFieldName": "", + "ApprovalStatusFieldName": "", "ApprovalDescriptionFieldName": "", + "ApprovalIsFilterUserName": false, "ApprovalIsResetWorkflow": false, "Criteria": [] } + }, + "IsDeletedField": true, + "IsCreatedField": true, + "InsertedRecords": { + "LanguageKeys": ["App.Mrp.Orders", "App.Mrp"], + "PermissionGroupNames": ["App.Mrp"], + "PermissionNames": ["App.Mrp.Orders", "App.Mrp.Orders.Create", "App.Mrp.Orders.Update", + "App.Mrp.Orders.Delete", "App.Mrp.Orders.Export", "App.Mrp.Orders.Import", + "App.Mrp.Orders.Note"], + "MenuCodes": ["App.Mrp", "App.Mrp.Orders"], + "DataSourceCodes": [] + } +} +``` + +> `EditorScript` alanındaki `// @builder` başlığı ile onu izleyen kod satırı **birebir +> tutarlı** olmalıdır. Elle yazmak yerine C# tarafında `EditorScript.Build(...)` kullan ya da +> Script Builder'da üret; uyuşmazsa dialog script'i "elle düzenlenmiş" sayar. + +### 4.4.1 Seeder'ın senin yerine ürettikleri — bunları dosyaya yazma + +`WizardDataSeeder`, dosyadaki `Wizard` bloğundan yola çıkarak aşağıdakileri **kendisi** üretir. +Bunları seed dosyasında aramaya kalkma, orada yokturlar: + +| Üretilen | Kural | +| --- | --- | +| Dil anahtarları | `{MenuCode}` (menü), `{ListFormCode}.Title`, `{ListFormCode}.Desc`, `{PermissionGroupName}` ve her alan için `CaptionName` | +| Yetkiler | `{MenuCode}` kökü + `.Create/.Update/.Delete` (+ List yolunda `.Export/.Import/.Note`) | +| Yetkilerin dil anahtarları | Ortak sabitler: `App.Platform.Create/Update/Delete/Export/Import/Note` | +| Üst menü | `MenuParentCode` yoksa yaratılır, `Order = max(kök Order) + 1` | +| Menü sırası | `MenuOrder` 0/boş ise `max(kardeş Order) + 1` | +| `DataSource` | `DataSourceCode` yoksa yaratılır; bağlantı metninde `Server` geçiyorsa `Mssql`, aksi hâlde `Postgresql` | +| `LayoutJson` | Görünüm bayrakları + `DefaultLayout` | +| `PermissionJson` / alan `PermissionJson` | `{MenuCode}` kökünden türetilir | +| `EditingFormJson` | `Groups` içinden; anahtar alan ve `IncludeInEditingForm: false` olanlar hariç | +| Form genişlik/yükseklik | Grup kolon sayısı ve satır sayısından hesaplanır (en fazla 3 kolon) | +| `FilterRowJson`, `HeaderFilterJson`, `SearchPanelJson`, `GroupPanelJson`, `ColumnOptionJson`, `PagerOptionJson`, `ExportJson` | Varsayılanlar | +| `DeleteCommand` | `IsDeletedField: true` ise soft delete `UPDATE` komutu; `false` ise `null` | +| `DefaultFilter` | `IsDeletedField: true` ise `"IsDeleted" = 'false'` | +| `InsertFieldsDefaultValueJson` | `IsCreatedField: true` ise `CreationTime=@NOW`, `CreatorId=@USERID`, `IsDeleted=false`, `Id=@NEWID`; değilse yalnızca `Id=@NEWID` | +| `DeleteFieldsDefaultValueJson` | `IsDeletedField: true` ise `DeleterId=@USERID`, `Id=@ID`; değilse yalnızca `Id=@ID` | +| `ValidationRuleJson` | `IsRequired: true` olan alanlara required kuralı | +| `LookupJson` | `LookupQuery` doluysa `LookupDataSourceType` + `DisplayExpr` + `ValueExpr` ile | +| `ShowNote` | Alt form veya iş akışı varsa açılır | +| `SelectionJson` | İş akışı varsa `single`, yoksa `none` | +| Görünüm option JSON'ları | İlgili bayrak açık **ve** zorunlu alanı dolu ise yazılır; eksikse görünüm sessizce kapanır | + +**Dikkat edilecek tuzaklar:** + +- `Tree`/`Gantt` için `ParentIdExpr`, `Scheduler` için `TextExpr`, `Todo` için `TitleExpr` **ve** + `StatusExpr` boşsa o görünümün JSON'u hiç yazılmaz — bayrağı `true` yapmak yetmez. +- Anahtar alanı (`KeyFieldName`) `Groups` içine koyarsan sütun olarak gizlenir ve forma girmez; + yine de tanımlaman doğrudur, çünkü tip bilgisi oradan okunur. +- `IsDeletedField: false` verirsen ekranda **kalıcı silme** olmaz (`DeleteCommand` null kalır); + soft delete kolonları olmayan tabloda silme istiyorsan ListForm editöründen komut yazmalısın. +- Dil anahtarı zaten varsa metni **ezilmez**. Yanlış metin görüyorsan anahtar başka bir yerde + tanımlanmıştır. + +### 4.5 Güncelleme, idempotency ve silme semantiği + +**Seeder idempotenttir — bir ekran zaten varsa dosya atlanır.** "Zaten var" ölçütü yola göre +değişir: + +| Yol | Kontrol | +| --- | --- | +| `List` | `ListForm` kaydında bu `ListFormCode` var mı | +| `Custom` + menülü | Bu `MenuCode` ile menü var mı | +| `Custom` + menüsüz | Bu `MenuCode` adında yetki var mı | + +Sonuç: **var olan bir ekranı seed dosyasını düzenleyerek değiştiremezsin.** Değiştirmenin üç +yolu vardır: + +1. Wizard ekranından `EditFileName` ile yeniden çalıştır (önce eski dosya ve + `InsertedRecords` kayıtları silinir, sonra yenisi üretilir) — **tercih edilen yol**. +2. ListForm editöründen ince ayar yap (seed dosyasına yansımaz, §9.6 uyarısı). +3. İlgili kayıtları elle sil, sonra seed'i yeniden çalıştır. + +> `List` yolunda seeder ekranı uygularken `ListForm`, `ListFormField` ve `ListFormWorkflow` +> kayıtlarını **silip yeniden yazar**; idempotency kapısını geçen dosya için bu güvenlidir. + +**Silme:** yalnızca `InsertedRecords` içindekiler silinir. Paylaşılan üst menü ve izin grubu +başka bir wizard tarafından yaratıldıysa kalır. Bu yüzden `InsertedRecords` listesine var olan +bir kaydı yazmak veri kaybettirir. + +### 4.6 Alt formlar ve widget'lar + +**`SubForms` (`SubFormDto`)** — ana kaydın altında sekme olarak açılan ekranlar: + +```jsonc +"SubForms": [ + { + "TabType": "List", // List | Tree | Gantt | Scheduler | Form | Chart + "TabTitle": "App.Mrp.OrderItems", // dil anahtarı + "Code": "App.Mrp.OrderItems", // hedef ListFormCode + "IsRefresh": true, // ana kayıt değişince yenilensin + "Relation": [ + { "ParentFieldName": "Id", "ChildFieldName": "OrderId", "DbType": 11 } + ] + } +] +``` + +`Relation` bir dizidir — bileşik anahtarda birden fazla eşleme verilir. + +**`Widgets` (`WidgetEditDto`)** — ekranın üstündeki KPI kartları: + +```jsonc +"Widgets": [ + { + "Title": "App.Mrp.Widget.OpenOrders", + "SubTitle": "App.Mrp.Widget.OpenOrdersSub", + "SqlQuery": "SELECT COUNT(*) FROM Mrp_T_Order WHERE IsDeleted = 'false' AND Status = 1", + "Value": "", // sabit değer; SqlQuery doluysa boş bırakılır + "ValueClassName": "text-3xl", + "Icon": "FcClock", + "Color": "blue", + "ColSpan": 1, + "ColGap": 16, + "ClassName": "", + "OnClick": "", // tıklanınca açılacak adres + "IsActive": true + } +] +``` + +`SqlQuery` **tek hücre** döndürmelidir; tenant kırılımı gerekiyorsa sorguya `TenantId` +koşulunu kendin yaz — widget sorgusu ListForm'un otomatik tenant filtresine tabi değildir. + +Alt form olarak kullanılacak ekranı **menüsüz wizard** (`CreateMenu: false`) ile üret; menüde +görünmesin ama kendi yetkileri ve alan tanımları olsun. Dosya adının zaman damgası ana +wizard'ınkinden **küçük** olmalıdır (§0.2). + +### 4.7 İş akışı (onay süreci) + +`WorkflowDto` onay alanlarını tabloda karşılığı olan kolonlara bağlar: + +| Alan | Tabloda karşılığı | +| --- | --- | +| `ApprovalUserFieldName` | Onaylayan kullanıcı | +| `ApprovalDateFieldName` | Onay tarihi | +| `ApprovalStatusFieldName` | Durum | +| `ApprovalDescriptionFieldName` | Onay/ret açıklaması | +| `ApprovalIsFilterUserName` | Liste yalnızca kullanıcının onayına düşenleri göstersin | +| `ApprovalIsResetWorkflow` | Kayıt güncellenince akış başa dönsün | + +`Criteria` düğüm grafiğidir. `Kind` değerleri: **`Start`**, **`Compare`**, **`Approval`**, +**`Inform`**, **`End`**. + +```jsonc +"WorkflowDto": { + "ApprovalUserFieldName": "ApproverId", + "ApprovalDateFieldName": "ApprovalDate", + "ApprovalStatusFieldName": "Status", + "ApprovalDescriptionFieldName": "ApprovalNote", + "ApprovalIsFilterUserName": true, + "ApprovalIsResetWorkflow": true, + "Criteria": [ + { "Id": "N1", "Kind": "Start", "Title": "Başlangıç", + "NextOnStart": "N2", "PositionX": 40, "PositionY": 40 }, + + { "Id": "N2", "Kind": "Compare", "Title": "Tutar kontrolü", + "CompareColumn": "Amount", "CompareOperator": ">", "CompareValue": 5000, + "CompareOutcomes": [ + { "Label": "> 5000", "TargetId": "N3", + "Conditions": [ { "CompareColumn": "Amount", "CompareOperator": ">", "CompareValue": 5000 } ] }, + { "Label": "<= 5000", "TargetId": "N4", + "Conditions": [ { "CompareColumn": "Amount", "CompareOperator": "<=", "CompareValue": 5000 } ] } + ], + "PositionX": 40, "PositionY": 160 }, + + { "Id": "N3", "Kind": "Approval", "Title": "Genel Müdür onayı", + "Approver": "", "NextOnApprove": "N5", "NextOnReject": "N6", + "PositionX": 300, "PositionY": 260 }, + + { "Id": "N4", "Kind": "Approval", "Title": "Yönetici onayı", + "Approver": "", "NextOnApprove": "N5", "NextOnReject": "N6", + "PositionX": 40, "PositionY": 260 }, + + { "Id": "N5", "Kind": "Inform", "Title": "Talep sahibine bilgi", + "Approver": "", "NextOnStart": "N7", "PositionX": 170, "PositionY": 380 }, + + { "Id": "N6", "Kind": "End", "Title": "Reddedildi", "PositionX": 340, "PositionY": 480 }, + { "Id": "N7", "Kind": "End", "Title": "Onaylandı", "PositionX": 170, "PositionY": 480 } + ] +} +``` + +Kurallar: + +- **Id sözleşmesi:** `N` ile başlayan kısa id'ler seeder tarafından `{ListFormCode}-{Id}` hâline + getirilir; hedef alanlar (`NextOn*`, `CompareOutcomes[].TargetId`) da aynı normalizasyondan + geçer. Bu yüzden dosyada `N1`, `N2` yazman yeterlidir ve **tekil olmaları şarttır**. +- `Compare` düğümünde çok dallı karar için `CompareOutcomes` kullanılır; iki dallı basit karar + için `NextOnTrue` / `NextOnFalse` yeterlidir. +- `Approval` düğümü `NextOnApprove` / `NextOnReject`, `Start` ve `Inform` düğümleri + `NextOnStart` alanını kullanır. `End` düğümünün çıkışı yoktur. +- `CompareValue` **sayısaldır** (`decimal`); metin karşılaştırması bu düğümle yapılmaz. +- `Title` tekil olmalıdır; seeder tekrar eden başlıkları ayrıştırır ama okunabilirliği bozar. +- Akış varsa ListForm `SelectionMode = single` olur ve not paneli otomatik açılır. +- Görsel tasarım `/admin/listform/edit/{kod}` → Workflow sekmesindedir. + +### 4.8 `ListForm` entity — List component'in tek kaynağı + +Bir List component'in **bütün** davranışı `ListForm` (ekran) ve `ListFormField` (alan) kayıtlarında +durur. Wizard bunları üretir; `/admin/listform/edit/{kod}` düzenler. Prompt ile üretim yaparken +wizard dosyası yazılır, ama hangi ayarın hangi kolona düştüğünü bilmek gerekir — çünkü wizard'ın +kapsamadığı bir istek geldiğinde cevap "kod yazalım" değil, "şu JSON kolonunu şöyle ayarlayalım" +olmalıdır. + +**Kimlik ve varyant** + +| Kolon | Ne yapar | +| --- | --- | +| `ListFormCode` | Ekranın kodu; kullanıcı/rol/dil özelleştirmeleri buna bağlanır | +| `CultureName` · `UserId` · `RoleId` | **Aynı ekranın varyantları.** Aynı `ListFormCode` ile ikinci bir kayıt açıp yalnızca bir rol ya da dil için farklı sütun/düzen verebilirsin | +| `ListFormType` | `List` · `Form` — ekranın türü | +| `IsSubForm` / `SubFormsListFormType` | Alt form olarak açıldığında davranış | + +**Veri** + +| Kolon | Ne yapar | +| --- | --- | +| `DataSourceCode` | Bağlantı | +| `SelectCommandType` + `SelectCommand` | Kaynak türü ve kaynağın kendisi | +| `TableName` | Tablo adı/alias — yazma komutlarının hedefi | +| `KeyFieldName` + `KeyFieldDbSourceType` | Anahtar | +| `SelectFieldsDefaultValueJson` | Select'e geçirilen varsayılan parametreler | +| `DefaultFilter` | Her sorgunun sonuna eklenen WHERE (soft delete filtresi buradadır) | +| `IsTenant` · `IsBranch` · `IsOrganizationUnit` | Otomatik kırılım filtreleri | +| `DataSourceJson` · `CommonJson` | Kaynak ve ortak ayarlar | + +**Yazma (CRUD)** + +| Kolon | Ne yapar | +| --- | --- | +| `InsertCommand` / `UpdateCommand` / `DeleteCommand` | Özel SQL komutları; boşsa platform üretir | +| `*BeforeCommand` / `*AfterCommand` | Komut öncesi/sonrası çalışan SQL kancaları — **stok düşme, log yazma, durum güncelleme gibi işleri kod yazmadan burada yaparsın** | +| `Insert/Update/DeleteFieldsDefaultValueJson` | Otomatik doldurulan alanlar (`@NOW`, `@USERID`, `@NEWID`, `@ID`) | +| `Insert/Update/DeleteServiceAddress` | Varsayılan `list-form-data/*`; özel endpoint ya da Dynamic Service'e yönlendirilebilir | +| `FormFieldsDefaultValueJson` | Form açılışındaki varsayılanlar | + +**Görünüm ve düzen — 8 layout** + +`LayoutJson` hangi layout'ların açık olduğunu ve varsayılanı taşır. Her layout'un kendi ayar +kolonu vardır: + +| Layout | Ayar kolonu | Zorunlu alan | +| --- | --- | --- | +| **Grid** | `ColumnOptionJson`, `RowJson`, `PagerOptionJson`, `SelectionJson`, `StateStoringJson` | — | +| **Card** | `ColumnOptionJson` (kart alanları sütun tanımlarından türer) | — | +| **Pivot** | `PivotOptionJson` + alan bazında `PivotSettingsJson` | — | +| **Chart** | `SeriesJson`, `LegendJson`, `ArgumentAxisJson`, `ValueAxisJson`, `TooltipJson`, `PanesJson`, `AnnotationsJson`, `CrosshairJson`, `ScrollBarJson`, `ZoomAndPanJson`, `SizeJson`, `MarginJson`, `TitleJson`, `AnimationJson`, `Common*Json` | `SeriesJson` | +| **Tree** | `TreeOptionJson` | `KeyExpr` + `ParentIdExpr` | +| **Gantt** | `GanttOptionJson` | `ParentIdExpr` + `TitleExpr` + `StartExpr` + `EndExpr` | +| **Scheduler** | `SchedulerOptionJson` | `TextExpr` + `StartDateExpr` + `EndDateExpr` | +| **Todo (Kanban)** | `TodoOptionJson` | `TitleExpr` + `StatusExpr` | + +Sekiz layout **aynı `SelectCommand` üzerinden** beslenir; ayrı sorgu, ayrı ekran, ayrı menü +gerekmez. Kullanıcı ekranın üstünden geçiş yapar, tercihi kullanıcı bazlı saklanır. + +Bu, tek bir metadata kaydından DevExtreme'in `DataGrid`, `CardView`, `PivotGrid`, `Chart`, +`TreeList`, `Gantt` ve `Scheduler` bileşenlerinin tamamının sürülmesi demektir. **Todo layout +Kanban görünümüdür** ve diğerleriyle aynı veri hattından beslenir — kolonları `StatusExpr` +değerlerinden üretir, kart sürüklendiğinde ilgili kolonu günceller. Yani "aynı veriyi pano +olarak da görelim" isteği yeni bir ekran değil, `Todo: true` + `TodoOptionDto` demektir. + +**Filtre, arama, düzenleme** + +| Kolon | Ne yapar | +| --- | --- | +| `FilterRowJson` · `HeaderFilterJson` · `FilterPanelJson` · `SearchPanelJson` · `GroupPanelJson` | Filtreleme/arama/gruplama panelleri | +| `ExtraFilterJson` | Ekrana özel ek filtre araç çubuğu | +| `EditingOptionJson` | Düzenleme modu (row/cell/batch/form/popup), başlık, boyut, izinler | +| `EditingFormJson` | Düzenleme formunun grup/sekme düzeni | +| `CommandColumnJson` | Satır aksiyon sütunu | +| `PermissionJson` | Ekran yetkileri (create/read/update/delete/export/import/note) | +| `SubFormsJson` · `WidgetsJson` · `WorkflowJson` | Alt formlar, KPI kartları, onay akışı | +| `CustomJsSourcesJson` · `CustomStyleSourcesJson` | Sayfa yüklenince çalışan JS/CSS — **son çare** | +| `Width` · `Height` · `FullHeight` · `AdaptiveLayoutJson` | Boyut ve uyarlanabilir yerleşim | +| `ExportJson` | xlsx/csv/pdf dışa aktarma | + +**`ListFormField` — alan başına ayarlar** + +| Grup | Kolonlar | +| --- | --- | +| Bağlama | `FieldName`, `SourceDbType`, `JoinTableJson` (kolon başka tablodan geliyorsa) | +| Görünüm | `CaptionName`, `PlaceHolder`, `Visible`, `IsActive`, `Width`, `ListOrderNo`, `Alignment`, `Format`, `BandName` | +| Sıralama/filtre | `SortIndex`, `SortDirection`, `AllowSearch`, `ColumnFilterJson`, `ColumnHeaderJson`, `GroupingJson`, `ColumnCustomizationJson` | +| Özet | `TotalSummaryJson`, `GroupSummaryJson` | +| Düzenleme | `EditorType2`, `EditorOptions`, `EditorScript`, `EditOrderNo`, `EditGroupOrderNo`, `ColSpan`, `AllowEditing`, `AllowAdding`, `ValidationRuleJson` | +| Lookup | `LookupJson` | +| Biçimlendirme | `ColumnStylingJson`, `ColumnCssClass`, `ColumnCssValue` | +| Pivot | `PivotSettingsJson` | +| Yetki | `PermissionJson` → `CanRead`, `CanCreate`, `CanUpdate`, `CanExport` | +| Varyant | `UserId`, `RoleId`, `CultureName` | + +> **Kural:** "Şunu da yapabilir miyiz?" sorusuna cevap ararken önce bu iki tabloya bak. Aradığın +> davranışın bir JSON kolonu varsa çözüm konfigürasyondur; `CustomJsSources` ve kod yazmak +> ondan sonra gelir. + +### 4.9 Toolbar ve satır butonları — `CommandColumnJson` + +Grid'in araç çubuğuna ve satır aksiyon sütununa **özel butonlar** eklenir. Süreç yönetiminin +(onaya gönder, iptal et, belge üret, dış sisteme aktar) kod yazmadan yapıldığı yer burasıdır. + +`ListForm.CommandColumnJson` bir `CommandColumnDto[]` taşır: + +```jsonc +[ + { + "ButtonPosition": 1, // 0 = satır aksiyon sütunu · 1 = toolbar + "AuthName": "App.Hr.AdvanceRequests.Update", // yetki yoksa buton hiç çizilmez + "Text": "App.Hr.AdvanceRequests.SendToApproval", // dil anahtarı (başına :: eklenir) + "Hint": "App.Hr.AdvanceRequests.SendToApprovalHint", + "Icon": "check", + "IsVisible": true, + "VisibleExpression": "", + "Url": "", + "UrlTarget": "_blank", + "DialogName": "", + "DialogParameters": "", + "OnClick": "" + } +] +``` + +Bir butonun davranışı **üç moddan biridir** ve bu sırayla değerlendirilir: + +| Mod | Dolu alan | Ne yapar | +| --- | --- | --- | +| **Adres** | `Url` (+ `UrlTarget`) | Adresi açar. `@FieldName` token'ları **seçili ilk satırın** kolon değerleriyle değiştirilir (`/admin/form/App.Hr.AdvanceRequests/@Id`). PWA modunda hedef `_self` olur. | +| **Diyalog** | `DialogName` + `DialogParameters` | Kayıtlı bir bileşeni diyalog olarak açar. `DialogParameters` bir JSON nesnesidir; `@Kolon` değerleri satırdan doldurulur (`{"requestId":"@Id","amount":"@Amount"}`). | +| **Script** | `OnClick` | Serbest JS. Son çaredir; önce diğer iki mod denenir. | + +Kurallar: + +- `AuthName` **zorunlu sayılmalıdır**: yetki kontrolünden geçmeyen buton hiç çizilmez. Yetkisi + olmayan kullanıcıya süreç butonu göstermek istemiyorsan tek gereken budur. +- `Text` ve `Hint` dil anahtarıdır; çeviri `::` öneki eklenerek çözülür. Düz metin yazma. +- Toolbar butonu **seçili satırlarla** çalışır; `SelectionJson` uygun modda (`single`/`multiple`) + olmalıdır. İş akışı varsa wizard bunu zaten `single` yapar. +- Satır bazlı aksiyon istiyorsan `ButtonPosition: 0` kullan; buton komut sütununda görünür. +- Onay akışının kendi butonları (Onayla/Reddet) `WorkflowJson` üzerinden **otomatik** gelir; + onları `CommandColumnJson` ile tekrar tanımlama. + +Tipik süreç kalıbı: `Url` modunda bir Custom Endpoint'i ya da Dynamic Service'i çağıran adres +verilir, dönüşte liste yenilenir. İş mantığı SQL/serviste kalır; ekranda yalnızca buton tanımı +durur. + +--- + +## 5. Artefakt B — `EditorScript` (alan davranışı) + +### 5.1 Script metninin şekli + +``` +// @builder {"v":1,"rules":[ … ]} ← ilk satır: kuralların JSON'u +// @runOnOpen ← yalnızca bir kural open/both ise + +``` + +Dialog script'i regex ile çözmez; başlıktaki kurallardan script'i **yeniden üretip** metinle +karşılaştırır. Aynı değilse script "elle düzenlenmiş" sayılır ve kural editörü kapanır. +Bu yüzden başlık ile gövde asla ayrı ayrı düzenlenmez. + +### 5.2 Tarifler (recipes) + +| Grup | `recipe` | Ürettiği | +| --- | --- | --- | +| calc | `multiply` | `set(T, round(num(A) * num(B), d))` | +| calc | `subtract` | `set(T, round(num(A) - num(B), d))` | +| calc | `percent` | oran alan ya da sabit; `mode` yok/`add`/`subtract` | +| calc | `sum` | `set(T, round(sum('A','B',…), d))` | +| calc | `formula` | serbest ifade: `set(T, num('Gross') * 0.18)` | +| calc | `today` | `set(T, bugün + offsetDays)` (`yyyy-MM-dd`) | +| calc | `days` | iki tarih arası gün (bitiş dahil) | +| calc | `hours` | iki saat arası fark; bitiş küçükse ertesi güne taşar | +| data | `copy` | seçili lookup kaydının kolonunu alana taşır | +| data | `setValue` | sabit/şablonlu değer (`{Alan}`, `{value}`, `{selected.Yol}`) | +| data | `clear` | verilen alanları boşaltır | +| view | `readOnly` | koşula göre kilitler (`invert` ile tersi) | +| interaction | `notify` | uyarı gösterir | +| interaction | `ask` | onay ister; vazgeçilirse alan eski değerine döner ve script durur | +| interaction | `openUrl` | adres açar | +| integration | `apiToField` | API çağırır, cevabın bir parçasını alana yazar | +| — | `custom` | serbest JS satırı (son çare) | + +Tetikleyici: `change` (varsayılan) · `open` (form açılışında varsayılan üretmek için) · `both`. +Koşullar `and`/`or` ile birleşir: `always`, `equals`, `notEquals`, `contains`, `empty`, +`notEmpty`, `greaterThan`, `lessThan`, `isTrue`, `isFalse`. + +### 5.3 Runtime API (custom/formula içinde kullanılabilir) + +| İmza | Ne yapar | +| --- | --- | +| `value` / `field` | Değişen alanın değeri ve adı | +| `get('Alan')` | Form verisinden okur (yol destekler) | +| `num()` / `str()` / `bool()` / `dateOf()` | Tip dönüşümü | +| `set('Alan', deger)` / `set({A:1,B:2})` | Yazar (toplu yazım tek flush) | +| `clear('A','B')` | Boşaltır | +| `copy('kolon','Alan')` | Seçili kayıttan taşır | +| `selected('Yol')` / `snum()` / `sstr()` | Lookup/GridBox seçili kaydı | +| `readOnly('Alan', kosul)` | Alanı kilitler | +| `round(x, 2)` / `sum('A','B')` | Matematik | +| `days('Bas','Bit')` / `hours('Bas','Bit')` | Tarih/saat farkı | +| `tpl('{Alan} - {value}')` | Şablon | +| `notify()` / `ask()` / `openUrl()` | Etkileşim | +| `await api('/api/x','data.name')` | HTTP | +| `isReady` | Script form açılışında mı çalışıyor | + +### 5.4 Seeder tarafı (C#) — tercih edilen yol + +`api/src/Sozsoft.Platform.Domain.Shared/Editors/` + +```csharp +EditorScript = EditorScript.Build( + EditorScript.Multiply("Quantity", "UnitPrice", "Total", digits: 2), + EditorScript.Percent("Total", "VatRate", "VatAmount", 2, EditorScriptPercentMode.Add) + .When(EditorScriptCondition.IsNotEmpty("VatRate")), + EditorScript.ReadOnly("Total"), + EditorScript.Today("OrderDate").OnOpen(), + EditorScript.Copy("Customer.TaxNumber", "TaxNumber"), + EditorScript.Ask("Fiyatı değiştirmek istediğinize emin misiniz?") + .When(EditorScriptCondition.GreaterThan("UnitPrice", "1000"))); +``` + +Tek kurallı script için `EditorScriptRule` doğrudan `string`'e dönüşür: + +```csharp +EditorScript = EditorScript.Days("StartDate", "EndDate", "DayCount"); +``` + +> **Eşleme kuralı:** `scriptRecipes.ts` ile bu sınıflar aynı çıktıyı üretmek zorundadır. Birinde +> bir tarif değişirse aynı görevde diğeri de değişir. + +--- + +## 6. Artefakt C — `EditorOptions` + +DevExtreme editörüne geçirilen JSON. UI tarafındaki sözlük `optionSpecs.ts`, hazır kalıplar +`presets.ts`, C# tarafı `EditorOptions` + `EditorOptionsBuilder`. + +| İhtiyaç | C# | Ürettiği JSON (özet) | +| --- | --- | --- | +| Pasif alan | `EditorOptions.Disabled()` | `{"disabled":true}` | +| Salt okunur | `EditorOptions.ReadOnly()` | `{"readOnly":true}` | +| Temizle düğmesi | `EditorOptions.ShowClearButton()` | `{"showClearButton":true}` | +| Çok satırlı | `EditorOptions.Multiline(80)` | `{"height":80}` | +| Sabit ondalık | `EditorOptions.Number(2)` | format + maske + spin | +| Yüzde | `EditorOptions.Percent()` | | +| Tarih / tarih-saat / saat | `EditorOptions.Date()` · `DateTime()` · `Time(15)` | | +| Telefon | `EditorOptions.Phone()` | global format + maske + örnek | +| Kaydırıcı | `EditorOptions.Slider()` | | +| Görsel yükleme | `EditorOptions.ImageUpload(multiple: true, 80, 80)` | | +| Zengin metin | `EditorOptions.Html(240)` | tam araç çubuğu | + +Akıcı ekleme: `.Placeholder()`, `.MaxLength()`, `.Height()`, `.HeightCss("100%")`, `.Width()`, +`.Format()`, `.FixedPoint(p)`, `.DisplayFormat()`, `.SerializationFormat()`, `.Mask()`, +`.Text()`, `.Number()`, `.Flag()`, `.Json()`. + +```csharp +EditorOptions = EditorOptions.Number(4).ShowClearButton(), +EditorOptions = EditorOptions.Multiline(60).Placeholder("Açıklama"), +EditorOptions = EditorOptions.New().Flag("acceptCustomValue", true), +``` + +Kurallar: + +- Aynı anahtar iki kez verilirse sonuncusu geçerlidir; anahtar sırası korunur. +- `optionSpecs.ts` içinde `platform: true` işaretli ayarlar backend'de tipli DTO'ya + (`GridBoxOptionsDto`, `TagBoxOptionsDto`, `ImageUploadOptionsDto`) çözülür; yanlış tip sessizce + yok sayılır. +- Preset'ler mevcut JSON ile **birleşir**, diğer ayarları silmez. + +--- + +## 7. Artefakt D — Custom Component (Visual Designer) + +ListForm ile ifade edilemeyen ekranın yolu **fiziksel React sayfası değil**, tasarımcıda +üretilen bir custom component'tir. + +### 7.1 Seed dosyası + +`configs/seeds/{kapsam}/custom/{Name}.json` + +```jsonc +{ + "GeneratedAt": "2026-08-20T11:23:45Z", + "CustomComponents": [ + { + "Name": "OrderBoard", + "RoutePath": "/admin/order-board", + "Code": "/*__SOZSOFT_VISUAL_DESIGNER____*/\nconst OrderBoard = () => { … }\n\nexport default OrderBoard", + "Props": "{\"visualDesigner\":{ … }}", + "Description": "", + "IsActive": true, + "Dependencies": ["OrderCard"], + "DataSources": [ + { + "Name": "Mrp_T_Order — GetList", + "Method": "GET", + "Url": "/api/app/crudendpoint/Mrp_T_Order", + "ResponsePath": null, + "EntityName": "Mrp_T_Order", + "OperationType": "GetList", + "SeedFile": "crud/Mrp_T_Order.json" + } + ] + } + ] +} +``` + +- `DataSources` elle yazılmaz: kaydetme sırasında `CustomComponentDataSourceResolver` + tasarımcı dokümanındaki `dataSources` listesini `method + path` ile CRUD endpoint kataloğunda + arayarak üretir. Eşleşenler `EntityName` + `SeedFile` taşır; elle yazılmış endpoint'ler listede + kalır ama bu alanları boş gelir. +- `Dependencies`, kanvasa bırakılan diğer custom component'lerin adlarıdır. + +### 7.1.1 İki mod: hangisini yazacaksın? + +**Runtime `Code` alanını her zaman kullanmaz.** `getComponentRuntimeCode` şu kararı verir: + +``` +Props.visualDesigner varsa && version === 1 && sourceMode === 'visual' && nodes bir dizi ise + → kod tasarımcı dokümanından YENİDEN ÜRETİLİR (generateDesignerCode), Code alanı yok sayılır +aksi hâlde + → Code alanı olduğu gibi Babel ile derlenir +``` + +Bu, prompt ile üretim için belirleyicidir: + +| Mod | Ne yazarsın | Ne zaman | +| --- | --- | --- | +| **Visual** (`sourceMode: "visual"`) | Yalnızca `Props.visualDesigner` dokümanı. `Code` alanı okunmaz; marker'lı bir yer tutucu bırakılır. | Ekran, tasarımcının bileşenleriyle ifade edilebiliyorsa. Kullanıcı sonradan kanvastan düzenleyebilir. **Varsayılan tercih.** | +| **Code** (`Props: null`) | Doğrudan JSX. Tasarımcı kanvası olmaz. | Tasarımcı sözlüğünün karşılamadığı yerleşim/etkileşim gerekiyorsa. | + +Visual modda `Code` alanına şunu yaz — hem marker'ı taşır (tasarımcı dosyayı açabilsin) hem de +derlense bile zararsızdır: + +``` +/*__SOZSOFT_VISUAL_DESIGNER____*/ +const {Name} = () => { + return ( + <> + + ) +} + +export default {Name} +``` + +Marker'ın içi `Props.visualDesigner` ile **aynı dokümanın** URL-encode edilmiş hâlidir. Tutarsız +kalırsa tasarımcı `Props`'u esas alır; yine de ikisini eşit tut. + +### 7.1.2 Code modunda çalışma ortamı + +Derlenen kod paylaşılan bir fonksiyon kapsamında çalışır. **`import` satırları temizlenir** — +ihtiyacın olan her şey kapsamdan gelir: + +| Kapsamdaki ad | Ne | +| --- | --- | +| `React` | React'in tamamı (`React.useState`, `React.useEffect`…) | +| `components/ui` dışa aktarımlarının **tamamı** | `Button`, `Input`, `Select`, `Dialog`, `Card`, `Notification`, `toast`, … (yeni bir UI bileşeni eklendiğinde otomatik gelir) | +| `UiKit` | Aynı kitin isim uzayı hâli | +| `PlatformViewHost` | Bir ListForm ekranını gömmek için | +| `apiService` | Kimlik doğrulamalı HTTP istemcisi — **tercih edilen yol** | +| `axios` | Ham HTTP (kimlik doğrulama eklemez) | +| `DOMPurify` | HTML temizleme | +| `translate(key)` | Dil anahtarı çözümü | +| `checkPermission(name)` | Yetki kontrolü | +| `getCulture()` | Aktif dil | + +Kurallar: + +- `export default {ComponentName}` zorunludur; ad dosya/kayıt adıyla birebir aynı olmalıdır. +- Kullanıcıya görünen metni gömme; `translate('App.…')` kullan ve dil anahtarını wizard + dosyasında ya da `LanguagesData.json`'da tanımla. +- Veriye erişimde `apiService` kullan; taban adres, token ve tenant başlığı ondan gelir. + Örneklerde gördüğün `axios.create({ baseURL: 'https://localhost:44344/' })` kalıbı **eski bir + kayıttır, taklit etme**. +- Yetkiyle gizlenecek her blok `checkPermission('App.…')` ile sarılır. Custom component + route'u `authority: []` ile üretilir (§7.6 uyarısı) — kontrol bileşenin içindedir. +- Başka bir custom component'i JSX olarak kullanabilirsin; adını `Dependencies` listesine ekle. + +### 7.2 Tasarımcı dokümanı (`Props.visualDesigner`) + +```ts +DesignerDocument = { + version: 1 + sourceMode: 'visual' | 'code' // 'code' tek yönlüdür, kanvasa dönülmez + nodes: DesignerNode[] + canvas: { width: 'responsive' | 'desktop' | 'tablet' | 'mobile' } + lifecycle: { onMount: string } // sayfa açılışında çalışan script + dataSources: DesignerDataSource[] + permissionCode?: string // Wizard'ın bu bileşen için ürettiği okuma yetkisi +} + +DesignerNode = { + id: string // 'cmp_…' — opak + type: string // 'Form', 'Grid', 'Input', 'ListView', 'FlexRow' … + kind: 'html' | 'ui' | 'layout' | 'platform' | 'custom' + slot?: string + ref?: string // 'btnSave' — script'ler birbirine bununla erişir + props: Record + events: Record // olay adı → script + bindings: Record + children: DesignerNode[] +} + +DesignerBinding = { + sourceId: string // 'source_…' (data source) ya da 'cmp_…' (kapsayıcı Form kaydı) + path: string // okunacak kolon + labelPath?: string + valuePath?: string + columns?: string[] // ekranda görünmeyen ek kolonlar; refs..getColumn('x') ile okunur +} + +DesignerDataSource = { + id: string + name: string + method: 'GET' | 'POST' | 'PUT' | 'DELETE' + url: string + responsePath: string // yanıt içindeki liste yolu; boşsa yanıtın kendisi + filters?: DesignerDataSourceFilter[] +} + +DesignerDataSourceFilter = { + id: string + field: string + operator: 'eq'|'ne'|'contains'|'startswith'|'endswith'|'gt'|'gte'|'lt'|'lte'|'in'|'isnull'|'notnull' + source: 'static' | 'query' | 'route' | 'record' + value: string // static → değer · query/route → URL parametre adı · record → . + previewValue?: string // yalnızca tasarımcı önizlemesi; üretilen koda girmez + required?: boolean // true ise değer yoksa istek hiç atılmaz +} +``` + +Filtre → query string sözleşmesi: `eq` çıplak parametredir (`?RoleId=5`), diğerleri adını son ek +olarak taşır (`?Name.contains=abc`). Bu, CrudEndpoint `GetList`'in okuduğu sözleşmedir. + +> `required: true` varsayılandır ve bilinçlidir: değeri henüz gelmemiş bir filtre isteği +> **tutmalıdır**; filtresiz koleksiyonu yüklemek "filtre çalışmıyor" gibi okunur. + +### 7.3 Toolbox + +| Aile | Bileşenler | +| --- | --- | +| `layout` | `PageContainer` (maxWidth, padding, gap), `FlexRow` (columns, firstColumnWidth, gap, wrap, align), `Spacer` (height) | +| `data` | `Form` — dört CRUD endpoint'ini sahiplenen kapsayıcı | +| `platform` | `ListView`, `DataGridView`, `TreeView`, `GanttView`, `TodoBoard`, `CardView`, `SchedulerView`, `PivotView`, `ChartView` — her biri `listFormCode` ile bir ListForm ekranını gömer | +| `html` / `ui` | Ham HTML etiketleri ve `components/ui` bileşenleri (sözleşmeleri metadata'dan okunur) | +| `custom` | Diğer custom component'ler | + +**Önce yeniden kullan:** istenen şey zaten bir ListForm ekranının verdiği liste/ağaç/grafikse, +onu primitiflerden yeniden kurma — ilgili `platform` düğümünü `listFormCode` ile bırak. + +### 7.3.1 Çalışan minimal doküman — bir Form + gömülü liste + +Aşağıdaki `Props` değeri (JSON string olarak yazılır) tek başına çalışır: üstte bir CRUD formu, +altında var olan bir ListForm ekranı. + +```jsonc +{ + "visualDesigner": { + "version": 1, + "sourceMode": "visual", + "canvas": { "width": "responsive" }, + "lifecycle": { "onMount": "" }, + "permissionCode": "App.Hr.AdvanceRequest", + "dataSources": [ + { "id": "source_list", "name": "Hr_T_AdvanceRequest — GetList", + "method": "GET", "url": "/api/app/crudendpoint/Hr_T_AdvanceRequest", "responsePath": "" }, + { "id": "source_byid", "name": "Hr_T_AdvanceRequest — GetById", + "method": "GET", "url": "/api/app/crudendpoint/Hr_T_AdvanceRequest/{id}", "responsePath": "" }, + { "id": "source_create", "name": "Hr_T_AdvanceRequest — Create", + "method": "POST", "url": "/api/app/crudendpoint/Hr_T_AdvanceRequest", "responsePath": "" }, + { "id": "source_update", "name": "Hr_T_AdvanceRequest — Update", + "method": "PUT", "url": "/api/app/crudendpoint/Hr_T_AdvanceRequest/{id}", "responsePath": "" }, + { "id": "source_delete", "name": "Hr_T_AdvanceRequest — Delete", + "method": "DELETE", "url": "/api/app/crudendpoint/Hr_T_AdvanceRequest/{id}", "responsePath": "" } + ], + "nodes": [ + { + "id": "cmp_page", "type": "PageContainer", "kind": "layout", "ref": "page", + "props": { "maxWidth": "1280px", "padding": 24, "gap": 16 }, + "events": {}, "bindings": {}, + "children": [ + { + "id": "cmp_form", "type": "Form", "kind": "layout", "ref": "form1", + "props": { + "title": "::App.Hr.AdvanceRequest.Title", + "selectEndpoint": "source_byid", + "insertEndpoint": "source_create", + "updateEndpoint": "source_update", + "deleteEndpoint": "source_delete", + "keyFieldName": "Id", + "collectionPath": "", + "keySource": "route", + "keyParamName": "id", + "autoLoad": true, + "showToolbar": true, + "columnCount": 2, + "gap": 16, + "selectPermission": "", + "insertPermission": "", + "updatePermission": "", + "deletePermission": "" + }, + "events": {}, "bindings": {}, + "children": [ + { + "id": "cmp_amount", "type": "Input", "kind": "ui", "ref": "inputAmount", + "props": { "size": "md" }, + "events": {}, + "bindings": { "value": { "sourceId": "cmp_form", "path": "Amount" } }, + "children": [] + }, + { + "id": "cmp_note", "type": "Input", "kind": "ui", "ref": "inputNote", + "props": { "textArea": true, "rows": 3 }, + "events": {}, + "bindings": { "value": { "sourceId": "cmp_form", "path": "Description" } }, + "children": [] + } + ] + }, + { + "id": "cmp_list", "type": "ListView", "kind": "platform", "ref": "listView1", + "props": { + "listFormCode": "App.Hr.AdvanceRequests", + "height": "520px", + "className": "mt-4", + "designerPermission": "App.Hr.AdvanceRequests", + "filters": [ + { + "id": "filter_1", "field": "EmployeeId", "operator": "eq", + "source": "record", "value": "form1.EmployeeId", + "previewValue": "", "required": true + } + ] + }, + "events": {}, "bindings": {}, "children": [] + } + ] + } + ] + } +} +``` + +Doküman yazarken uyulacak kurallar: + +- **`id` benzersiz ve kalıcıdır.** Bağlamalar (`bindings.sourceId`) bu id'lere işaret eder; + değiştirirsen bağ kopar. `cmp_…` / `source_…` öneki bir sözleşmedir, zorunlu değildir ama koru. +- **`ref` script'lerin adres defteridir**; olay script'leri birbirine `refs.inputAmount…` ile + erişir. Okunur ve tekil tut. +- Form içindeki bir alanın kaynağı **Form düğümünün `id`'sidir** (`sourceId: "cmp_form"`), + data source değil. Data source'a bağlanan şeyler listeler/seçim bileşenleridir. +- Her düğümde `props`, `events`, `bindings`, `children` **var olmalıdır** (boş olsalar bile). +- `kind` değeri toolbox ailesiyle uyumlu olmalıdır; yanlış `kind` düğümü çizdirmez. +- **`::` öneki dil anahtarıdır.** Metin taşıyan bir prop `::` ile başlıyorsa üretilen kodda + `translate('::…')` çağrısına dönüşür; başlamıyorsa düz metin olarak gömülür. Kullanıcıya + görünen her metni `::App.…` biçiminde yaz (`"title": "::App.Hr.AdvanceRequest.Title"`). +- **`platform` düğümleri `filters` prop'u alır.** Gömülü ListForm ekranını ana kayda ya da URL'ye + bağlamak için `DesignerDataSourceFilter` ile aynı şekli kullanır; `source: "record"` iken + `value` `"{formRef}.{Kolon}"` biçimindedir (`"form1.EmployeeId"`). `required: true` ise ana + kayıt gelmeden liste yüklenmez. +- Anahtar kolon adında **büyük/küçük harf endpoint'in döndürdüğüyle aynı olmalıdır** + (`keyFieldName: "id"` ile `"Id"` farklı şeylerdir); CRUD endpoint'in yanıtına bak. + +### 7.4 `Form` bileşeni + +| Prop | Anlam | +| --- | --- | +| `selectEndpoint` / `insertEndpoint` / `updateEndpoint` / `deleteEndpoint` | **Data source `id`'si** (`"source_5w_5ibpv"`) — URL değil. Endpoint'in kendisi `dataSources` listesinde tanımlıdır. | +| `keyFieldName` | Anahtar kolon | +| `collectionPath` | Select yanıtında liste yolu | +| `keySource` + `keyParamName` | Anahtarın nereden geldiği (route/query/sabit) | +| `previewKeyValue` | Yalnızca tasarımcı önizlemesi | +| `autoLoad` | Açılışta Select çalışsın mı | +| `showToolbar` | Kaydet/Sil araç çubuğu | +| `columnCount` / `gap` | İç yerleşim | +| `title` | Dolu ise Card başlığı; boş ise başlık düşer | + +İçine bırakılan her bileşen Select sonucunun bir kolonuna bağlanır (`bindings.value.sourceId` = +Form düğümünün `id`'si) ve Save/Delete üzerinden geri yazar. Olaylar: `onLoad`, +`onRecordChange`, `onFieldChange`, `onNewRecord`, `onModeChange`, `onBeforeSave`, `onAfterSave`, +`onBeforeDelete`, `onAfterDelete`, `onError`. + +### 7.5 Yetki modeli (zorunlu) + +İki katman: + +1. **Düğüm görünürlüğü** — `props.designerPermission` dolu olan düğüm, yetki yoksa hiç + render edilmez. Veriyi göremeyecek kullanıcıya boş bir Grid göstermek yerine bileşen çizilmez. +2. **Form komutları** — `selectPermission` / `insertPermission` / `updatePermission` / + `deletePermission`. + - `Otomatik` mod: temel, dokümandaki `permissionCode`'dur (Wizard'ın bu bileşen için ürettiği + okuma yetkisi, menü kaydının korunduğu yetkinin aynısı). Komutlar sırasıyla `''`, + `.Create`, `.Update`, `.Delete` son eklerini alır. + - `Özel` mod: yetki adı elle yazılır. + - Boş bırakılan komut serbesttir. + +Manager ekranı yetkileri: `App.DeveloperKit.Components{,.Create,.Update,.Delete}`. + +### 7.6 Bileşeni menüye bağlama + +Bileşen tek başına bir ekran değildir; menü ve yetki Wizard'ın `Custom` yolundan gelir: + +```jsonc +{ + "Wizard": { + "ComponentKind": 1, + "WizardName": "OrderBoard", + "ListFormCode": "App.Mrp.OrderBoard", + "MenuCode": "App.Mrp.OrderBoard", + "CustomComponentName": "OrderBoard", + "MenuUrl": "/admin/order-board", + "CreateMenu": true, + "MenuParentCode": "App.Mrp", + "PermissionGroupName": "App.Mrp", + "PermissionGroupDisplayNameEn": "MRP", "PermissionGroupDisplayNameTr": "MRP", + "LanguageTextMenuEn": "Order Board", "LanguageTextMenuTr": "Sipariş Panosu", + "MenuIcon": "FcKanban", "MenuOrder": 2 + }, + "IsDeletedField": false, "IsCreatedField": false, + "InsertedRecords": { + "LanguageKeys": ["App.Mrp.OrderBoard"], + "PermissionGroupNames": [], + "PermissionNames": ["App.Mrp.OrderBoard", "App.Mrp.OrderBoard.Create", + "App.Mrp.OrderBoard.Update", "App.Mrp.OrderBoard.Delete"], + "MenuCodes": ["App.Mrp.OrderBoard"], "DataSourceCodes": [] + } +} +``` + +Custom yolunda `ListForm` **üretilmez**; `Title`/`Desc` dil anahtarları da üretilmez. +`Export`/`Import`/`Note` yetkileri bu yolda yoktur. + +> **Güvenlik — route yetkisiz üretilir.** Custom component route'ları `RoutePath` değerinden +> türetilir ve `authority: []` ile kaydedilir; `/admin/` ile başlayan yol `protected`, diğerleri +> `public` olur. Yani **URL'yi bilen her oturum açmış kullanıcı sayfayı açabilir**. Menü yetkisi +> yalnızca menüde görünmeyi engeller. Gerçek koruma bileşenin içindedir: +> `designerPermission` ile düğümleri gizle, Form komutlarını `*Permission` ile kapat, kod modunda +> `checkPermission()` ile sar. Yetkisiz kullanıcıya boş bir sayfa gösteren bir bileşen doğru +> sonuçtur; veri gösteren bileşen açıktır. +> +> `RoutePath` `/admin/` ile başlamıyorsa sayfa **herkese açıktır** — bilinçli değilse başlat. + +### 7.6.1 Depodaki gerçek örnek — `RoleComponent` + `RoleList` wizard + +Bu çift, custom component'in menüye nasıl bağlandığının çalışan örneğidir. İncelenecek dosyalar: + +``` +configs/seeds/host/custom/RoleComponent.json +configs/seeds/host/crud/AbpRoles.json · AbpUsers.json · AbpUserRoles.json +configs/seeds/host/wizard/20260819131500_RoleList.json +``` + +Nasıl bağlanıyorlar: + +| Bağ | Değer | +| --- | --- | +| Wizard `ComponentKind` | `1` (Custom) | +| Wizard `CustomComponentName` | `RoleComponent` | +| Component `RoutePath` | `/admin/roles` | +| Wizard `MenuUrl` | `/admin/roles` — component'in `RoutePath` değeriyle **aynı** | +| Wizard `MenuCode` / `ListFormCode` | `App.Wizard.RoleList` | +| Component `Props.visualDesigner.permissionCode` | `App.Wizard.RoleList` — wizard'ın ürettiği okuma yetkisi | +| Wizard `InsertedRecords.PermissionNames` | `App.Wizard.RoleList` + `.Create` + `.Update` + `.Delete` | +| Wizard `InsertedRecords.LanguageKeys` | `App.Wizard.RoleList` | +| Component `DataSources[].SeedFile` | `crud/AbpRoles.json` vb. | + +**Okunacak dersler:** + +1. `permissionCode` **wizard'ın ürettiği yetkinin aynısıdır**. Form komutları `Otomatik` modda + bu koda `.Create`/`.Update`/`.Delete` ekleyerek çözülür. Component'i yazarken bu değeri + wizard'daki `MenuCode` ile aynı yap. +2. `RoutePath` ile wizard'ın `MenuUrl` değeri **birebir aynı** olmalıdır; farklıysa menü başka + bir yere gider. +3. Component'in `DataSources` listesi `crud/*.json` dosyalarına `SeedFile` ile işaret eder — + export zip'inin bu dosyaları da toplamasını sağlayan şey budur. +4. Wizard `Custom` yolunda olsa bile `ListFormCode`, `Grid/Card/Pivot/Chart` bayrakları gibi + List alanlarını taşır; **bunlar kullanılmaz**, varsayılan değerleriyle dosyada kalır. + Yeni dosya yazarken bunları temizlemeye çalışma, şablonu koru. +5. `platform` düğümündeki `listFormCode` (`AbpIdentity.Roles`) **var olan bir ListForm kodudur**; + component ekranın kendisini yeniden kurmaz, gömer. + +### 7.7 Taşıma + +Bir ekranı başka ortama taşımak tek dosya kopyalamak değildir. Wizard File Manager'ın **Export** +butonu zip üretir: + +``` +wizard/{dosya}.json +custom/{component}.json (bağımlılıklarıyla) +crud/{entity}.json (component'lerin kullandığı endpoint'ler) +{sql|postgres}/{object|execute}/{nesne}.sql (List yolunda SelectCommand'ın nesnesi) +``` + +Import iki adımlıdır: analiz (`New`/`Identical`/`Conflict`, çakışmalar diff editörde çözülür) → +uygulama (dosya dosya yazım, önceki hâl yedeklenir, `RollbackImport` ile toplu geri alma). +Sınırlar: yalnızca `wizard`, `crud`, `custom`, `sql`, `postgres` kök klasörleri (SQL sağlayıcı +klasörlerinin altında yalnızca `object`/`execute`); 5 MB/dosya, 50 MB/arşiv, 500 girdi. +Yetkiler: `App.Listforms.Wizard.Export` / `.Import`. + +--- + +## 8. Artefakt E — SQL nesneleri + +### 8.1 Tablo tasarımcısı — varsayılan kolonlar + +Kullanıcı yalnızca iş kolonlarını söylese bile şunlar varsayılan olarak eklenir: +`Id`, `TenantId`, `CreationTime`, `CreatorId`, `LastModificationTime`, `LastModifierId`, +`IsDeleted`, `DeletionTime`, `DeleterId`. + +Kullanıcı açıkça "tenant yok" / "audit yok" demedikçe çıkarma; her seferinde sorma. +Deploy öncesi adımda CRUD endpoint üretimi ve hangi operasyonların aktif başlayacağı seçilir; +seçilmeyenler pasif kaydedilir, sonradan diyalogdan açılabilir. + +### 8.2 View tasarımcısı + +Bir ekran birden fazla tablodan besleniyorsa, `SelectCommandType: 4` (Query) yerine **tasarlanmış +bir view** (`SelectCommandType: 2`) tercih edilir: yeniden kullanılabilir, seed'lenir ve +tasarımcıda görsel olarak düzenlenebilir kalır. + +Model: kaynaklar (tablo/view, `CROSS`/`OUTER APPLY`, türetilmiş alt sorgu), JOIN'ler +(`INNER`/`LEFT`/`RIGHT`/`FULL`/`CROSS`; `=`, `<>`, `>`, `>=`, `<`, `<=`) ve criteria satırları +(Column / Alias / Output / Group By / Sort / Filter / Or…). Group By sütunu: `GroupBy`, `Where` +(satır çıktıya girmez, yalnızca filtre taşır), `SUM`, `COUNT`, `COUNT_DISTINCT`, `AVG`, `MIN`, +`MAX`. Filtre hücreleri serbest yüklemdir (`> 100`, `LIKE '%abc%'`, `IS NULL`). + +**Tek yönlüdür.** Model → T-SQL. Var olan bir view kanonik şekle uymuyorsa geri okunamaz ve +dialog ham SQL moduna düşer. Bu yüzden tasarlanmış bir view'ı elle düzenlemek tasarımcıyı +kaybettirir. + +Nesneler `configs/seeds/{kapsam}/{sql|postgres}/{object|execute}/{ad}.sql` altına yazılır: +`object` = yeniden oluşturulabilir nesne (tablo/view/fonksiyon/prosedür), `execute` = bir kez +çalıştırılacak script. + +### 8.3 SQL seed dosyası yazım kuralları + +| Kural | Açıklama | +| --- | --- | +| Dosya adı | Nesne adıyla **birebir aynı**: `Hr_T_AdvanceRequest.sql` | +| Klasör | `object` = idempotent nesne tanımı · `execute` = nesne olarak da oluşturulur, ayrıca tüm migration'lar bittikten sonra çalıştırılır | +| Sağlayıcı | Hedef SQL Server ise `sql/`, PostgreSQL ise `postgres/`. **İkisini birden destekliyorsan iki dosya da yazılır**; içerikler aynı nesnenin iki diyalektidir. | +| İdempotency | SQL Server: `CREATE OR ALTER VIEW/PROCEDURE/FUNCTION`, tablolar için `IF NOT EXISTS` koruması. PostgreSQL: `CREATE OR REPLACE VIEW/FUNCTION`, `CREATE TABLE IF NOT EXISTS`. | +| Şema | `dbo` kullanılır; PostgreSQL tarafında tanımlayıcılar çift tırnaklıdır (`dbo."Hr_T_AdvanceRequest"`) çünkü kolon adları PascalCase'tir. | +| Toplu ayraç | SQL Server'da prosedür sonunda `GO` kullanılabilir; PostgreSQL'de kullanılmaz. | +| Yorum | Dosyanın ilk satırında ne ürettiğini yaz (`-- Create View`). | + +Tablo yazarken §8.1'deki varsayılan kolonlar dahil edilir. Örnek iskelet (SQL Server): + +```sql +-- Create Table +IF OBJECT_ID(N'[dbo].[Hr_T_AdvanceRequest]', N'U') IS NULL +BEGIN + CREATE TABLE [dbo].[Hr_T_AdvanceRequest] ( + [Id] UNIQUEIDENTIFIER NOT NULL CONSTRAINT [PK_Hr_T_AdvanceRequest] PRIMARY KEY, + [TenantId] UNIQUEIDENTIFIER NULL, + -- iş kolonları buraya + [CreationTime] DATETIME2 NOT NULL, + [CreatorId] UNIQUEIDENTIFIER NULL, + [LastModificationTime] DATETIME2 NULL, + [LastModifierId] UNIQUEIDENTIFIER NULL, + [IsDeleted] BIT NOT NULL CONSTRAINT [DF_Hr_T_AdvanceRequest_IsDeleted] DEFAULT (0), + [DeletionTime] DATETIME2 NULL, + [DeleterId] UNIQUEIDENTIFIER NULL + ); +END +``` + +> `Id` tipini ne seçtiysen wizard dosyasındaki `KeyFieldDbSourceType` ile aynı olmalıdır +> (`UNIQUEIDENTIFIER` → `9`, `INT IDENTITY` → `11`). Uyumsuzluk sessiz veri hatası üretir. + +### 8.4 Seeder bu dosyaları nasıl çalıştırır + +Yeni bir entity/tablo `sql/object/` altına yazılır ve veritabanı `SqlDataSeeder` tarafından bu +dosyalardan kurulur. Mekanik: + +1. **Yalnızca aktif sağlayıcının klasörü çalışır.** `DefaultDatabaseProvider` SQL Server ise + `{kapsam}/sql/`, PostgreSQL ise `{kapsam}/postgres/`. İki dosya da yazmak taşınabilirlik + içindir; aynı anda ikisi birden çalışmaz. +2. Sıra: `object/` → `execute/`, her klasörün içinde **dosya adına göre alfabetik**. Bir tablo + başka bir tabloya FK ile bağlıysa dosya adları o sırayı vermelidir (ör. `Hr_T_Employee.sql` + `Hr_T_EmployeeAdvance.sql`'den önce gelir). +3. Dosya içeriği `GO` satırlarından batch'lere bölünür ve sırayla çalıştırılır. PostgreSQL'de + `GO` kullanılmaz. +4. **Hata seed'i durdurur.** Bir batch patlarsa istisna yukarı fırlatılır ve o kapsamın seed'i + yarım kalır. Bu yüzden her script tekrar tekrar çalıştırılabilir (idempotent) olmalıdır: + `CREATE OR ALTER` / `CREATE OR REPLACE` / `IF NOT EXISTS`. +5. SQL nesneleri bittikten hemen sonra **aynı kapsam için `CrudDataSeeder`** çalışır; böylece + CRUD endpoint'leri dayandıkları tablolardan sonra oluşur. Tablo dosyasını yazmadan + `crud/{Entity}.json` yazmak anlamsızdır. +6. Klasör kökünde kalmış `.sql` dosyaları geriye dönük uyumluluk için çalıştırılır ama **uyarı + loglar**; yeni dosyayı her zaman `object/` ya da `execute/` altına koy. + +**`execute/` klasörünün özel sözleşmesi:** buradaki dosyalar iki aşamalıdır. + +- 1. aşama: `SqlDataSeeder` dosyayı çalıştırır → nesne oluşur/güncellenir. +- 2. aşama: tüm migration ve seed işlemleri bittikten sonra `AfterAllMigrationsSqlExecutor` + **dosya adından türettiği stored procedure'ü çağırır**. + +Yani `execute/` altındaki bir dosya, **adı prosedür adıyla aynı olan bir stored procedure +tanımlamalıdır** (`Hr_T_AdvanceRequest_Backfill.sql` → `Hr_T_AdvanceRequest_Backfill` +prosedürü). `schema.Procedure.sql` biçimi de desteklenir. Prosedür adı güvenli tanımlayıcı +deseninden geçer (harf/alt çizgi ile başlar, en fazla 127 karakter). Sadece bir DDL çalıştırmak +istiyorsan dosya `object/` altına aittir; `execute/` veri dolumu ve migration sonrası düzeltme +içindir. + +### 8.5 Yeni entity/tablo eklerken izlenecek yol + +1. `sql/object/{Modul}_T_{Entity}.sql` — tabloyu §8.3 iskeletiyle yaz (tenant + audit kolonları + varsayılan). PostgreSQL hedefi de destekleniyorsa `postgres/object/` altına diyalekt eşini yaz. +2. İndeks ve FK'leri **aynı dosyada**, varlık kontrolüyle ekle (`IF NOT EXISTS`); ayrı dosya + sıralamayı kırılgan hâle getirir. +3. Ekran bu tabloyu doğrudan kullanacaksa CRUD endpoint gerekmez — ListForm `SelectCommandType: 1` + ile tabloya bağlanır. Custom Component kullanacaksa `crud/{Entity}.json` de yaz (§9). +4. Wizard dosyasında `SelectCommand` = tablo adı, `KeyFieldName` = `Id`, + `KeyFieldDbSourceType` = tablodaki `Id` tipinin sayısı, `IsTenant` = tabloda `TenantId` varsa + `true`, `IsDeletedField`/`IsCreatedField` = ilgili kolonlar varsa `true`. +5. DB Migrate çalıştır; log'da `Executing: CREATE TABLE …` satırını gör. + +--- + +## 9. Artefakt F — CRUD Endpoint + +Konum: `configs/seeds/{kapsam}/crud/{EntityName}.json` · Okuyan: `CrudDataSeeder` + +```jsonc +{ + "EntityName": "Mrp_T_Order", + "GeneratedAt": "2026-08-20T06:13:28Z", + "Endpoints": [ + { "Method": "GET", "Path": "/api/app/crudendpoint/Mrp_T_Order", "OperationType": "GetList", "IsActive": true }, + { "Method": "GET", "Path": "/api/app/crudendpoint/Mrp_T_Order/{id}", "OperationType": "GetById", "IsActive": true }, + { "Method": "POST", "Path": "/api/app/crudendpoint/Mrp_T_Order", "OperationType": "Create", "IsActive": true }, + { "Method": "PUT", "Path": "/api/app/crudendpoint/Mrp_T_Order/{id}", "OperationType": "Update", "IsActive": true }, + { "Method": "DELETE", "Path": "/api/app/crudendpoint/Mrp_T_Order/{id}", "OperationType": "Delete", "IsActive": true } + ] +} +``` + +Üretilen C# kodu dosyada tutulmaz; entity adı ve operasyondan yeniden türetilir. + +- Üretim/aktivasyon yeri: SQL Query Manager nesne gezgininde tablo satırının **CRUD Endpoints** + aksiyonu, toolbar'daki toplu üretim, tablo tasarımcısının deploy öncesi adımı ve Wizard'ın + veri ayarları adımı — hepsi aynı `CrudEndpointDialog`. +- Yetki: `App.SqlQueryManager.CrudEndpoints`. İzin yoksa butonlar görünmez ve + `crud-endpoint-generate` uçlarının tamamı reddedilir. +- Dispatcher üzerinden çağrı kapıları: `App.DeveloperKit.CustomEndpoints.Get/.Post/.Put/.Remove`; + endpoint bazlı User/Role/Global kuralları bunun üstünde denetlenir. +- `GetList` filtre sözleşmesi §7.2'deki ile aynıdır. + +--- + +### 9.2 Custom Endpoint — elle yazılmış SQL'i API olarak açmak + +Ekran: `/admin/list/App.DeveloperKit.CustomEndpoints` (bir ListForm ekranıdır). +Entity: `CustomEndpoint`. + +| Alan | Anlam | +| --- | --- | +| `Name` / `Description` | Tanımlayıcı | +| `Url` | Yayınlanacak yol | +| `Method` | `GET` / `POST` / `PUT` / `DELETE` | +| `DataSourceCode` | Hangi bağlantıdan okunacağı. **`"!Tenant"`** özel değeridir: isteği yapan kullanıcının kendi tenant veritabanından okunur. | +| `Sql` | Parametreli sorgu. String birleştirme yasak. | +| `ParametersJson` | `CustomEndpointParameter[]` | +| `PermissionsJson` | `CustomEndpointPermission[]` | + +```jsonc +// ParametersJson +[ + { "Type": "Query", "Name": "customerId", "DefaultValue": "", "Path": "", "IsRequired": true }, + { "Type": "Path", "Name": "id", "DefaultValue": "", "Path": "", "IsRequired": true }, + { "Type": "Body", "Name": "note", "DefaultValue": "", "Path": "data.note", "IsRequired": false }, + { "Type": "Static","Name": "tenantMode", "DefaultValue": "1", "Path": "", "IsRequired": true } +] + +// PermissionsJson — Global / Role / User +[ + { "ResourceType": "Role", "ResourceId": "SalesManager" }, + { "ResourceType": "User", "ResourceId": "" } +] +``` + +Parametre tipleri: `Static` (sabit), `Query` (query string), `Path` (URL segmenti), +`Body` (gövde; `Path` ile gövde içindeki yol). + +Yetki iki katmanlıdır: önce dispatcher kapısı +(`App.DeveloperKit.CustomEndpoints.Get/.Post/.Put/.Remove`), sonra endpoint'in kendi +`PermissionsJson` kuralları. Endpoint tanımlarını düzenleme yetkileri ise ListForm ekranının +`App.DeveloperKit.CustomEndpoints{,.Create,.Update,.Delete}` yetkileridir. + +**Ne zaman CRUD, ne zaman Custom Endpoint?** Tek tabloya standart CRUD gerekiyorsa CRUD Endpoint +(otomatik üretilir, seed'lenir, filtre sözleşmesi hazırdır). Birleştirilmiş/şekillendirilmiş +sorgu, rapor ya da özel bir yol gerekiyorsa Custom Endpoint. + +### 9.3 Dynamic Service — çalışma zamanında derlenen C# servisi + +Ekran: `/admin/developerkit/dynamic-services`. Entity: `DynamicService`. +Bu, karar sırasının **3. adımıdır**; konfigürasyon ve SQL yetmediğinde gelir, kod değişikliğinden +önce denenir. + +| Alan | Anlam | +| --- | --- | +| `Name` | Benzersiz servis adı (`DynamicCustomerService`) | +| `DisplayName` / `Description` | Kullanıcı dostu başlık ve açıklama | +| `Code` | Tam C# sınıfı — `using`'ler ve `namespace` dahil | +| `ControllerName` | Swagger'da görünecek controller adı | +| `PrimaryEntityType` | Kullandığı ana entity (opsiyonel) | +| `IsActive` | Yayında mı | +| `CompilationStatus` | `Pending(0)` · `Success(1)` · `Failed(2)` · `InProgress(3)` | +| `LastCompilationError` / `LastSuccessfulCompilation` | Son derleme sonucu | +| `Version` / `CodeHash` | Kod her değiştiğinde `Version` artar, hash yenilenir | + +Yaşam döngüsü: **TestCompile** (Roslyn ile derle, hata mesajı UI'a döner) → **Publish** → +`DynamicAssemblyRegistrationService` assembly'yi tenant bağlamıyla kaydeder → +`ActionDescriptorChangeProvider` MVC'ye route tablosunun değiştiğini bildirir → +`DynamicControllerActivator` bağımlılıkları enjekte eder. **Uygulama yeniden başlatılmaz.** + +Yetkiler: `App.DeveloperKit.DynamicServices{.Create,.Edit,.Delete,.Manage,.TestCompile,.Publish,.ViewCode}`. + +Yazarken `dotnet.instructions.md` §5 geçerlidir: `PlatformAppService` türet, `[Authorize(...)]` +ile koru, tenant bağlamını `ICurrentTenant` üzerinden kullan, SQL'i parametreli çalıştır, +kullanıcıya görünen metni `PlatformResource` anahtarından ver, sabit/secret gömme. + +--- + +### 9.4 Developer Kit menüsü — tam kapsam + +Menü: `App.DeveloperKit` (üst) → aşağıdaki alt ekranlar. Her satır bu dokümandaki bölümüne +bağlıdır; hiçbiri "kod yaz" adımının yerine geçmez, hepsi ondan öncedir. + +| Ekran | Route | Yetki | Ne üretir | Bölüm | +| --- | --- | --- | --- | --- | +| **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 | +| **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 | + +### 9.5 SQL Query Manager — bileşen bileşen + +Tek ekranda toplanmış araç seti (`SqlQueryManager.tsx`): + +| Panel / araç | Dosya | Yapabildikleri | +| --- | --- | --- | +| **Veri kaynağı seçici** | — | `DataSource` kayıtları arasında geçiş; SQL Server ve PostgreSQL | +| **Nesne gezgini** | `SqlObjectExplorer.tsx` | Tablolar, view'lar, stored procedure'ler, fonksiyonlar ve hazır şablonlar (`SqlObjectExplorerDto`); arama, nesne tanımını açma, kopyalama, silme, satır bazında **CRUD Endpoints** aksiyonu, çoklu seçimle toplu endpoint üretimi | +| **Sorgu editörü** | `SqlEditor.tsx` | Monaco tabanlı; çalıştırma (`ExecuteQueryAsync`), çoklu sekme, nesne tanımını editöre yükleme (`GetNativeObjectDefinitionAsync`), tablo create script'i üretme (`GetTableCreateScriptAsync`) | +| **Sonuç grid'i** | `SqlResultsGrid.tsx` | Sonuç kümesi görüntüleme ve dışa aktarma | +| **Tablo tasarımcısı** | `SqlTableDesignerDialog.tsx` | Kolon/anahtar/index tasarımı, varsayılan tenant + audit kolonları, deploy öncesi CRUD endpoint seçimi | +| **View tasarımcısı** | `SqlViewDesignerDialog.tsx` + `sqlViewDesigner/` | Diagram + criteria grid + T-SQL önizleme; bkz. 8.2 | +| **CRUD endpoint diyaloğu** | `CrudEndpointDialog.tsx` | Endpoint üretme, aktif/pasif etme, test, silme; aynı diyalog Wizard'ın veri adımında da açılır | 9 | +| **DB Migrate** | `DbMigrateButton` | Migration + seed tetikleme (`App.Setup.Migrate`) | +| **File Manager kısayolu** | `/admin/files` | `configs/seeds` altındaki seed dosyalarının yönetimi | +| **Kolon bilgisi** | `GetTableColumnsAsync` | Wizard alan adımının ve tasarımcıların kolon kaynağı | + +Servis yüzeyi (`SqlObjectManagerAppService`): `GetAllObjectsAsync`, `ExecuteQueryAsync`, +`GetNativeObjectDefinitionAsync`, `GetTableColumnsAsync`, `GetTableCreateScriptAsync`. + +Kalıcılık kuralı: tasarımcıdan çıkan her nesne +`configs/seeds/{kapsam}/{sql|postgres}/{object|execute}/{ad}.sql` altına düşer. +`object` yeniden oluşturulabilir nesneler (tablo/view/fonksiyon/prosedür), `execute` bir kez +çalışacak script'ler içindir. Editörde elle çalıştırılan ama dosyaya düşmeyen bir DDL, +veritabanı yeniden kurulduğunda kaybolur. + +### 9.6 ListForm ekranı ve editörü + +Wizard bir ekranı **üretir**; ince ayar `/admin/listform/edit/{listFormCode}` üzerinden yapılır. +Sekmeler ve karşılık geldikleri JSON kolonları: + +| Sekme | JSON | +| --- | --- | +| Veritabanı | `SelectCommandType`, `SelectCommand`, `KeyFieldName`, `Insert/Update/DeleteCommand` (+ `Before`/`After`), `*ServiceAddress` | +| Sütunlar | `ListFormField` kayıtları, `ColumnOptionJson`, banded/command sütunlar | +| Filtreler | `FilterRowJson`, `HeaderFilterJson`, `FilterPanelJson`, `SearchPanelJson`, `GroupPanelJson`, `ExtraFilterJson` | +| Düzenleme | `EditingOptionJson`, `EditingFormJson` | +| Yetkiler | `PermissionJson` (ekran ve sütun düzeyi) | +| Alt Form | `SubFormsJson` | +| Pivot / Tree / Gantt / Scheduler / Todo | `PivotOptionJson`, `TreeOptionJson`, `GanttOptionJson`, `SchedulerOptionJson`, `TodoOptionJson` | +| Widget | `WidgetsJson` | +| Workflow | `WorkflowJson` | +| Durum | `StateStoringJson`, `PagerOptionJson`, `SelectionJson` | +| Grafik | `SeriesJson`, `LegendJson`, `ArgumentAxisJson`, `ValueAxisJson`, `TooltipJson`, `ZoomAndPanJson` | + +Ayrıca `CustomJsSourcesJson` / `CustomStyleSourcesJson` ekran yüklendiğinde çalışacak JS/CSS +taşır — son çaredir, önce `EditorScript`/`EditorOptions` denenmelidir. + +> **Uyarı:** Bu ekrandan yapılan değişiklikler doğrudan veritabanına yazılır ve wizard seed +> dosyasına **yansımaz**. Kalıcı olması gereken bir değişikliği ya wizard'ı `EditFileName` ile +> yeniden çalıştırarak ya da seed dosyasını güncelleyerek yap. + +--- + +### 9.7 Menü Yönetimi ekranları + +Menü: `App.Menus` (üst) → dört ekran. **Wizard bunların çoğunu senin yerine yapar**; bu ekranlar +elle düzeltme ve wizard'ın kapsamadığı düzenlemeler içindir. + +| Ekran | Route | Yetki | Ne yapar | +| --- | --- | --- | --- | +| **Routes** | `/admin/list/App.Menus.Routes` | `App.Menus.Routes` | Fiziksel React sayfalarının route kayıtları (`Key`, `Path`, `ComponentType`, `ComponentPath`, `RouteType`, `Authority[]`) | +| **Menu Groups** | `/admin/list/App.Menus.MenuGroup` | `App.Menus.MenuGroup` | Menü grubu adları (`Erp`, `Kurs` gibi üst kümeler) | +| **Menu List** | `/admin/list/App.Menus.Menu` | `App.Menus.Menu` | Menü kayıtlarının ham listesi | +| **Menu Manager** | `/admin/menuManager` | `App.Menus.Manager` | Sürükle-bırak menü ağacı; sıra ve hiyerarşi düzenleme | + +**`Menu` kaydının alanları** (wizard bunları üretir; elle eklemen gerekirse): + +| Alan | Anlam | +| --- | --- | +| `Code` | Benzersiz kod; yetki adının kökü | +| `DisplayName` | **Dil anahtarı** (düz metin değil) | +| `ParentCode` | Üst menü; boş ise kök menü | +| `Url` | Açılacak adres | +| `Icon` | react-icons adı | +| `Order` | Kardeşler arası sıra | +| `ShortName` | Kısa rozet (`HR`, `MRP`) — kök menülerde kullanılır | +| `RequiredPermissionName` | Görünürlük yetkisi | +| `Target` / `CssClass` / `ElementId` / `IsDisabled` | Bağlantı davranışı ve görünüm | +| `UserId` / `RoleId` / `CultureName` | Kullanıcı, rol ve dil bazlı menü varyantı | + +**Kurallar — prompt ile üretim yaparken:** + +1. **Menüyü elle ekleme.** Yeni bir ekranın menüsü wizard seed dosyasından gelir; `MenusData.json` + yalnızca platformun kendi ekranları içindir ve orada da `Routes` bölümüne dokunulmaz. +2. **Route kaydı üretme.** Dinamik liste ekranları statik `/admin/list/:listFormCode` route'unu + kullanır; custom component'lerin route'u `RoutePath` alanından türetilir. `Route` tablosu + yalnızca fiziksel React sayfaları içindir ve prompt ile ekran üretirken böyle bir sayfa + yazılmaz. +3. **Menü sırası** için wizard'ın `MenuOrder` alanını kullan; Menu Manager'da elle taşımak seed + dosyasına yansımaz ve veritabanı sıfırlandığında kaybolur. +4. **Rol/dil bazlı menü varyantı** gerekiyorsa Menu List ekranından ikinci bir kayıt açılır — + aynı `Url`, farklı `RoleId`/`CultureName`. Wizard bunu üretmez. + +--- + +### 9.8 Wizard Manager — layout matrisi + +Wizard Manager platformun en yetenekli üretim ekranıdır: tek bir tanımdan **hem Custom Component +sayfası hem de yedi farklı görünümü olan List ekranı** üretir. Bir `List` wizard'ında birden fazla +görünüm aynı anda açık olabilir; kullanıcı ekranın üstünden geçiş yapar, tercihi kullanıcı bazlı +saklanır. + +| Görünüm | Bayrak | `DefaultLayout` | Zorunlu option alanı | Ne zaman aç | +| --- | --- | --- | --- | --- | +| Grid | `Grid` | `"grid"` | — | Neredeyse her zaman; varsayılan | +| Card | `Card` | `"card"` | — | Görsel/özet kayıtlar, mobil kullanım | +| Pivot | `Pivot` | `"pivot"` | — | Sayısal kırılım/analiz ihtiyacı | +| Chart | `Chart` | `"chart"` | — (grafik ayarları ListForm editöründe) | Trend/dağılım | +| Tree | `Tree` | `"tree"` | `TreeOptionDto.KeyExpr` + `ParentIdExpr` | Kendine referanslı hiyerarşi | +| Gantt | `Gantt` | `"gantt"` | `GanttOptionDto.ParentIdExpr` + `TitleExpr` + `StartExpr` + `EndExpr` | Zaman çizelgesi, bağımlılıklı görevler | +| Scheduler | `Scheduler` | `"scheduler"` | `SchedulerOptionDto.TextExpr` + `StartDateExpr` + `EndDateExpr` | Takvim/randevu | +| TodoBoard (Kanban) | `Todo` | `"todo"` | `TodoOptionDto.TitleExpr` + `StatusExpr` | Durum akışı olan işler | + +Kurallar: + +- Zorunlu option alanı boşsa seeder o görünümün JSON'unu **yazmaz**; bayrak açık kalsa bile + görünüm çalışmaz. Açtığın her görünümün option bloğunu doldur. +- `DefaultLayout`, bayrağı `true` olan bir görünüm olmalıdır. +- Aynı veri birden fazla görünümde aynı `SelectCommand` üzerinden sunulur; ayrı sorgu yazılmaz. +- Bir ekranın hem liste hem pano hem takvim olarak istenmesi **tek wizard dosyasıdır** — ikinci + bir ekran açma. + +Wizard Manager'ın kendi aksiyonları: liste/kart görünümü, arama, düzenleme (`EditFileName`), +silme, DB Migrate, Export (zip), Import (diff + rollback). Yetkiler +`App.Listforms.Wizard{,.Create,.Update,.Delete,.Export,.Import}`. + +--- + +## 10. Uçtan uca reçeteler + +### 10.1 "Var olan bir tablo için ekran aç" + +1. Tablo yoksa SQL Table Designer ile oluştur (varsayılan kolonlarla). +2. Ekran birden çok tablodan besleniyorsa View Designer ile view üret. +3. Wizard: menü/kimlik → veri ayarları (`SelectCommandType` + `SelectCommand` + anahtar) → + alanlar → (gerekiyorsa) alt form/widget/workflow → yayınla. +4. Hesaplanan alanlara `EditorScript`, biçimli alanlara `EditorOptions` ver. +5. Doğrula: menü görünüyor mu, yetkiler roldeki kullanıcı için doğru mu, tenant filtresi + çalışıyor mu, seed dosyası oluştu mu. + +### 10.2 "Ana-detay ekranı" + +1. Detay ekranını **menüsüz wizard** ile üret (`CreateMenu: false`). +2. Ana ekranın `SubForms` listesine `ParentFieldName` → `ChildFieldName` eşlemesini ve `DbType` + ekle. +3. İki ekranın yetkileri ayrıdır; detayın yetkisi verilmemişse sekme görünmez. + +### 10.3 "Serbest yerleşimli sayfa / dashboard" + +1. Gerekli endpoint'ler yoksa CRUD Endpoint üret. +2. Component Manager → yeni bileşen → Visual Designer: + `PageContainer` → `FlexRow` → içine `platform` düğümleri (`listFormCode` ile) ve/veya `Form`. +3. Veri kaynaklarını Data sekmesinden bağla; filtreleri `route`/`query`/`record` kaynaklarıyla + kur, `required` bayrağını bilinçli seç. +4. Görünürlük ve komut yetkilerini ver (`designerPermission`, `*Permission`). +5. Wizard `ComponentKind: 1` ile menüye bağla. +6. Doğrula: `custom/{Name}.json` ve ilgili `crud/*.json` dosyaları oluştu mu. + +### 10.4 "Kanban panosu" + +1. Tabloda durum kolonu olsun (`Status`). +2. Wizard'da `Todo: true`, `DefaultLayout: "todo"`, `TodoOptionDto` içinde `TitleExpr`, + `StatusExpr`, `StatusOrder` (kolon sırası), `AssigneeExpr`, `DueDateExpr` doldur. +3. Kolonlar `statusExpr` değerlerinden üretilir; kartlar sürüklenerek durum değiştirir, panodan + yeni kolon eklenebilir. + +--- + +### 10.5 "Rapor sorgusunu API olarak aç" + +1. Sorguyu SQL Query Manager'da yaz ve çalıştır, sonucu doğrula. +2. Tekrar kullanılacaksa view olarak deploy et (`sql/object/…`), tek seferlikse Custom Endpoint + içinde bırak. +3. Custom Endpoints ekranından kaydı oluştur: `Url`, `Method`, `DataSourceCode` (kullanıcının + kendi tenant veritabanı isteniyorsa `"!Tenant"`), `Sql`, `ParametersJson`, `PermissionsJson`. +4. Parametreleri `Query`/`Path`/`Body` olarak tanımla; hiçbirini SQL'e string birleştirme ile + koyma. +5. Erişimi `PermissionsJson` ile daralt; dispatcher kapısının (`…CustomEndpoints.Get` vb.) + ilgili rolde açık olduğunu doğrula. + +### 10.6 "Konfigürasyonla çözülemeyen iş mantığı" + +1. Önce gerçekten çözülemediğini göster: ListForm + SQL + Custom Endpoint neden yetmiyor, + bir iki cümleyle yaz. +2. Dynamic Service oluştur; `PlatformAppService` türet, `[Authorize(...)]` ile koru. +3. **TestCompile** → hatasız → **Publish**. `CompilationStatus` `Success` olmadan yayınlama. +4. Swagger'da `ControllerName` altında göründüğünü doğrula. +5. Ekran tarafında bu servisi ya ListForm'un `*ServiceAddress` alanından ya da Custom + Component'in bir data source'undan tüket. + +### 10.7 Şablon senaryo — "onaylı talep formu" (avans, izin, masraf, satın alma…) + +Bu tür isteklerin tamamı aynı iskeleti kullanır. Kullanıcı "avans talep formu istiyorum" dediğinde +üretilecek dosya seti: + +``` +configs/seeds/host/ +├── sql/object/Hr_T_AdvanceRequest.sql ← tablo (+ PostgreSQL hedefse postgres/object/…) +├── crud/Hr_T_AdvanceRequest.json ← yalnızca Custom Component yolunda gerekir +├── custom/AdvanceRequestForm.json ← yalnızca serbest yerleşim isteniyorsa +├── wizard/20260826101000_AdvanceRequestApprovals.json ← (varsa) menüsüz alt ekran, önce +└── wizard/20260826101500_AdvanceRequests.json ← ana ekran + menü + yetki + dil + onay akışı +``` + +**Karar:** Talep formu **standart bir kayıt listesi + düzenleme formu + onay akışı** ise +`ComponentKind: 0` (List) yeter ve `custom/` + `crud/` dosyalarına hiç gerek yoktur. Custom +Component yoluna yalnızca serbest yerleşim (özet kartlar, çok adımlı sihirbaz, gömülü panolar) +isteniyorsa geçilir. + +**Adım adım:** + +1. **Tablo** — iş kolonları + §8.1 varsayılanları. Onay akışı için dört kolon şarttır: + durum (`Status`), onaylayan (`ApproverId`), onay tarihi (`ApprovalDate`), açıklama + (`ApprovalNote`). Talep sahibi için de bir kolon (`EmployeeId`) bulunmalıdır. +2. **Wizard dosyası** — §4.4'teki iskelet: + - `IsTenant: true` (tabloda `TenantId` var), `IsDeletedField: true`, `IsCreatedField: true` + - `MenuParentCode` = modül kökü (`App.Hr`); yoksa wizard yaratır, `Order = max + 1` + - `PermissionGroupName` = `App.Hr`, EN + TR görünen adlarıyla + - Görünümler: `Grid` + `Card` açık; durum akışı görsel isteniyorsa `Todo: true` + + `TodoOptionDto` (§9.8) +3. **Alanlar** — `Groups` içinde iki grup mantıklıdır: "Talep" (tutar, tarih, açıklama) ve + "Onay" (durum, onaylayan, onay tarihi, onay notu). Onay grubundaki alanlar + `EditorOptions: "{\"readOnly\":true}"` ile kilitlenir; kullanıcı doldurmaz, akış doldurur. +4. **Hesap ve varsayılanlar** — `EditorScript` ile: + - Talep tarihi form açılışında bugün: `today` tarifi, tetikleyici `open` + - Tutar × oran gibi hesaplar: `multiply` / `percent` + - Eşik üstü tutarda uyarı: `notify` ya da `ask`, koşulu `greaterThan` +5. **Onay akışı** — §4.7'deki `WorkflowDto`. Tipik grafik: + `Start → Compare (tutar eşiği) → Approval (yönetici) / Approval (üst yönetici) → Inform → End`. + `ApprovalIsFilterUserName: true` ile herkes yalnızca kendi onayına düşenleri görür, + `ApprovalIsResetWorkflow: true` ile kayıt güncellenince akış başa döner. +6. **Yetki** — wizard `App.Hr.AdvanceRequests` kökünü ve `.Create/.Update/.Delete/.Export/ + .Import/.Note` alt yetkilerini üretir. Onaylayanların rolüne kök + `.Update` verilir; + talep sahiplerine kök + `.Create` yeter. +7. **Dil** — menü/başlık/açıklama metinleri wizard dosyasındaki `LanguageText*` alanlarından, + alan başlıkları her `Items` öğesinin `TurkishCaption`/`EnglishCaption` alanlarından üretilir. + Ayrıca `LanguagesData.json`'a dokunmaya gerek yoktur. +8. **Teslim** — dosyaları listele, DB Migrate gerektiğini söyle, §11 kontrol listesini geç. + +**Bu iskeletin türevleri:** izin talebi (tutar yerine gün sayısı, `days` tarifi), masraf talebi +(alt form olarak masraf kalemleri → menüsüz wizard + `SubForms`), satın alma talebi (kalemler + +tedarikçi lookup'ı + iki kademeli onay). + +--- + +## 11. Teslim öncesi kontrol listesi + +- [ ] Menü, route ve ekran sözleşmesi birbirini gösteriyor. +- [ ] Kök yetki + aksiyon yetkileri var; menü `RequiredPermissionName` ile bağlı. +- [ ] Yetki grubunun EN + TR görünen adı var. +- [ ] Kullanıcıya görünen her metnin EN + TR dil anahtarı var; anahtar tekrar edilmemiş. +- [ ] `IsTenant` (ve gerekiyorsa `IsBranch`/`IsOrganizationUnit`) doğru; sorgular tenant güvenli. +- [ ] Tüm SQL parametreli; string birleştirme yok. +- [ ] `DeleteCommand = DefaultDeleteCommand("{Tablo}")`. +- [ ] Seed dosyası oluştu; `InsertedRecords` yalnızca gerçekten yaratılan kayıtları içeriyor. +- [ ] Bağımlılıklar (custom component, crud endpoint, sql nesnesi) da seed'li. +- [ ] Export zip'i başka bir ortamda açıldığında ekran ayağa kalkıyor. +- [ ] Geri alma yolu yazılı: wizard dosyasını sil → `InsertedRecords` temizlenir. + +Prompt ile üretim yaptıysan ek olarak: + +- [ ] Dosyalar doğru kapsam klasöründe (`host/` ya da `tenants/{tenantId}/`). +- [ ] Üretim sırası doğru (§0.2) ve bağımlı wizard'ın zaman damgası daha büyük. +- [ ] Tablo/kolon adları uydurulmadı; ya var olan nesneden okundu ya da `sql/object/` altında + üretildi. +- [ ] `KeyFieldDbSourceType`, tablodaki `Id` tipiyle uyumlu. +- [ ] Açılan her görünümün zorunlu option alanı dolu (§9.8). +- [ ] Custom component'te route yetkisiz üretildiği için koruma bileşenin içinde + (`designerPermission` / `*Permission` / `checkPermission`). +- [ ] Custom component visual modda ise `Props.visualDesigner` ile `Code` marker'ı aynı dokümanı + taşıyor. +- [ ] Menü ve route elle eklenmedi; wizard dosyasından geliyor. +- [ ] Kullanıcıya hangi dosyaların üretildiği ve DB Migrate gerektiği söylendi. +- [ ] Ekran tarifi (§0.6) çıkarıldı ve yol seçimi gerekçelendirildi. +- [ ] Görselden üretildiyse: platformda karşılığı olmayan öğeler ve yapılan varsayımlar + (tablo/kolon adları, anahtar tipi, lookup kaynakları) açıkça bildirildi. +- [ ] Onaycı/iş kuralı gibi görselden okunamayan kritik bilgiler için soru soruldu (§0.7.4). diff --git a/README.md b/README.md index 643b0713..e206df7d 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ React/C# kodu yazmadan, yalnızca konfigürasyon ile mümkündür. ``` ┌──────────────────────────────────────────────────────────────────────┐ -│ UI — React 18 + Vite + TypeScript + DevExtreme + Tailwind (PWA) │ +│ UI — React 19 + Vite + TypeScript + DevExtreme + Tailwind (PWA) │ │ ├─ DynamicRouter (route kayıtlarından React Router üretimi) │ │ ├─ ListForm runtime (Grid / Pivot / Tree / Chart / Gantt / ...) │ │ ├─ Form runtime (DevExtreme Form, metadata ile alan üretimi)│ @@ -128,10 +128,10 @@ DevExtreme bileşenleri bu metadata'ya göre kendini kurar. | Bileşen | Sürüm / Notlar | | --- | --- | -| React | 18.3 | +| React | 19.2 | | Build | Vite 8 + TypeScript 5.9 | -| UI kütüphaneleri | DevExtreme 25.2 (React), Tailwind CSS 3.4, kendi `components/ui` tasarım sistemi | -| Routing | react-router-dom 6 (dinamik route üretimi) | +| UI kütüphaneleri | DevExtreme 26.1 (React), Tailwind CSS 3.4, kendi `components/ui` tasarım sistemi | +| Routing | react-router-dom 7 (dinamik route üretimi) | | State | easy-peasy (global store) + @tanstack/react-query | | Form | Formik + Yup, DevExtreme Form | | Kod editörü | Monaco Editor | @@ -183,7 +183,10 @@ sozsoft-platform/ │ │ # tenants/{tenantId}/{crud,custom,wizard,sql,postgres} │ │ # {sql|postgres}/{object,execute} │ └── ai/ # AI entegrasyonu (n8n akış tanımları) -├── .github/instructions/ # ai.instructions.md (bağlayıcı platform kuralları), list.instructions.md +├── .github/instructions/ # ai.instructions.md (bağlayıcı platform kuralları) +│ # lowcode.instructions.md (artefakt üretim referansı: şemalar, örnekler) +│ # dotnet.instructions.md (api/ için bağlayıcı .NET/ABP kod standardı) +│ # list.instructions.md (modül/liste ekleme prosedürü) ├── CLAUDE.md # Claude Code için operasyon kuralları └── README.md ``` @@ -383,11 +386,17 @@ Ekran davranışı büyük ölçüde JSON kolonlarında saklanır. Öne çıkanl Aynı `ListForm` tanımı, aynı veri hattı üzerinden birden fazla görünümle sunulabilir: -`Grid` · `Pivot` · `Tree` · `Chart` · `Gantt` · `Scheduler` · `CardView` · `TodoBoard` · `Widget Group` +`Grid` · `Pivot` · `Tree` · `Chart` · `Gantt` · `Scheduler` · `CardView` · `TodoBoard (Kanban)` · +`Widget Group` Kullanıcı görünümler arasında geçiş yapabilir; grid durumu (sütun sırası, genişlik, filtre, gruplama) kullanıcı bazlı saklanır ve sıfırlanabilir. +**TodoBoard artık bir Kanban panosudur:** kolonlar `TodoOptionJson` içindeki `statusExpr` +değerlerinden üretilir, kartlar kolonlar arasında sürüklenerek durum değiştirir ve panodan yeni +kolon (durum) eklenebilir. Kart içeriği `titleExpr`, `descriptionExpr`, `dueDateExpr`, `tagExpr` +ve atanan kullanıcı alanlarından kurulur. + ### 7.4. Alan (field) davranışı - **Editör tipleri:** `dxTextBox`, `dxTextArea`, `dxNumberBox`, `dxDateBox`, `dxDateRangeBox`, @@ -398,9 +407,67 @@ gruplama) kullanıcı bazlı saklanır ve sıfırlanabilir. - **Doğrulama:** `ValidationRuleJson` ile required/range/pattern/custom kuralları. - **Editör script:** `EditorScript` alanı ile alan değişiminde çalışan JS (örn. tarih farkından gün hesaplama, miktar × birim fiyat toplamı). Çalıştırma `utils/editorScriptRuntime.ts` üzerinden yapılır. +- **Editör seçenekleri:** `EditorOptions` alanı DevExtreme editörüne geçirilen JSON'dur + (format, maske, placeholder, yükseklik, buton görünürlüğü, platform editörlerinin tipli + ayarları). Elle JSON yazmak zorunlu değildir; bkz. 7.4.1. - **Koşullu biçimlendirme:** `ColumnStylingJson`, `ColumnCssClass`/`ColumnCssValue`. - **Alan bazlı yetki:** `PermissionJson` ile sütun düzeyinde okuma/yazma/dışa aktarma kontrolü. +#### 7.4.1. Script Builder ve Editor Options Builder + +`EditorScript` ve `EditorOptions` alanları hem görsel olarak hem de seeder kodundan üretilir; +**iki tarafın çıktısı birebir aynı olmak zorundadır.** + +**Editor Script Builder** (`json-row-operations/editor-script/`) + +- Kural sözlüğü `scriptRecipes.ts` içindedir; her tarif (recipe) tek satırlık runtime çağrısı üretir. + Gruplar: hesap (`calc`), veri (`data`), görünüm (`view`), bildirim, API. +- Üretilen script'in ilk satırındaki `// @builder {...}` başlığı kuralları saklar. Dialog script'i + regex ile çözmeye çalışmaz, bu başlıktan geri okur; başlık bozulursa/elle düzenlenirse script + "manuel" sayılır ve kural editörü kapanır. +- Kural tetikleyicileri: `change` (varsayılan), `open` (form açılışında — varsayılan değer üretmek + için, script başına `// @runOnOpen` direktifi eklenir), `both`. +- Koşullar `and`/`or` ile birleşir: `equals`, `notEquals`, `contains`, `empty`, `notEmpty`, + `greaterThan`, `lessThan`, `isTrue`, `isFalse`, `always`. +- Dialog'un kendisi ortaktır (`components/scriptBuilder/ScriptBuilderDialog.tsx` + IntelliSense); + ListForm lehçesi `formScriptDialect.ts`, Visual Designer lehçesi `designerScriptDialect.ts` + ile aynı sözleşmeye bağlanır. + +**Seeder tarafı (C#)** — `Sozsoft.Platform.Domain.Shared/Editors/` + +`scriptRecipes.ts` ve editorOptions sözlüğünün C# portudur. Seeder ile basılan script/JSON, +dialogda açılıp görsel olarak düzenlenebilir kalır. Bir tarif TypeScript tarafında değişirse +buradaki karşılığı da güncellenmelidir. + +| Tip | Kullanım | +| --- | --- | +| `EditorScript` | `Multiply`, `Subtract`, `Percent`, `Sum`, `Formula`, `Today`, `Days`, `Hours`, `Copy`, `SetValue`, `Clear`, `ReadOnly`, `Notify`, `Ask`, `OpenUrl`, `ApiToField`, `Custom` → `EditorScript.Build(rules)` | +| `EditorScriptRule` | `.When(...)` / `.WhenAny(...)` koşulları, `.OnChange()` / `.OnOpen()` / `.OnOpenAndChange()` tetikleyicisi; `string`'e örtük dönüşür | +| `EditorScriptCondition` | `Is`, `IsNot`, `Contains`, `IsEmpty`, `IsNotEmpty`, `GreaterThan`, `LessThan`, `IsTrue`, `IsFalse`, `Always` | +| `EditorOptions` | Hazır başlangıçlar: `Disabled()`, `ReadOnly()`, `ShowClearButton()`, `Multiline(h)`, `Number(precision)`, `Percent()`, `Date()`, `DateTime()`, `Time(interval)`, `Phone()`, `Slider()`, `ImageUpload()`, `Html(h)` | +| `EditorOptionsBuilder` | Akıcı ekleme: `.Placeholder()`, `.MaxLength()`, `.Height()`, `.Width()`, `.Format()`, `.Mask()`, `.Flag()`, `.Text()`, `.Number()`, `.Json()`; `string`'e örtük dönüşür | + +```csharp +EditorOptions = EditorOptions.Number(4).ShowClearButton(), +EditorOptions = EditorOptions.Multiline(60).Placeholder("Açıklama"), +EditorScript = EditorScript.Build( + EditorScript.Multiply("Quantity", "UnitPrice", "Total"), + EditorScript.Percent("Total", "VatRate", "VatAmount", mode: EditorScriptPercentMode.Add) + .When(EditorScriptCondition.IsNotEmpty("VatRate"))), +``` + +**Editor Options Builder** (`json-row-operations/editor-options/`) + +- UI tamamen `optionSpecs.ts` veri sözlüğünden üretilir; yeni bir ayar eklemek için oraya tek + satır yazmak yeterlidir. Her ayar `path` (örn. `format.precision`), tip (`boolean` üç durumlu / + `number` / `text` / `select` / `size` / `stringList` / `json`), grup ve hangi editörlerde + anlamlı olduğu bilgisini taşır. +- `platform: true` işaretli ayarlar backend'in tipli DTO'ya (`GridBoxOptionsDto`, + `TagBoxOptionsDto`, `ImageUploadOptionsDto`) deserialize ettiği alanlardır; yanlış tipte + yazılırsa sessizce yok sayılır. +- `presets.ts` hazır kalıplar sunar (HTML editör tam araç çubuğu, telefon maskesi, tarih/saat…); + kalıp mevcut JSON ile **birleştirilir**, diğer ayarları silmez. + ### 7.5. İş akışı ve onay `FormTabWorkflow` ve `views/admin/listForm/workflow/WorkflowDesigner.tsx` üzerinden görsel @@ -411,15 +478,57 @@ butonlar gösterilebilir. ### 7.6. Yeni ekran ekleme akışı (Wizard) -`/admin/listform/wizard` altındaki 11 adımlı sihirbaz, aşağıdaki yedi artefaktı tek akışta üretir: +`/admin/listform/wizard` altındaki sihirbaz, aşağıdaki artefaktları tek akışta üretir: 1. **ListForm** kaydı (kod, ad, başlık, veri kaynağı, select komutu, anahtar alan) 2. **ListFormField** kümesi (sütunlar, editörler, lookup'lar, doğrulamalar) 3. **Route** kaydı (`key`, `path`, `componentPath`, `routeType`, `authority`) 4. **Menu** kaydı (`ParentCode`, `Code`, `DisplayName`, `Url`, `Icon`, `RequiredPermissionName`, `Order`) 5. **Permission** kayıtları (`.Default`, `.Create`, `.Update`, `.Delete`, `.Export`, `.Import`, `.Note`) -6. Gerekiyorsa **ayar/entegrasyon** bağımlılıkları -7. **Doğrulama ve geri alma** notları +6. **Dil anahtarları** (menü, başlık, açıklama; EN + TR) +7. Gerekiyorsa **ayar/entegrasyon** bağımlılıkları +8. **Seed dosyası** — `configs/seeds/{host|tenants/{tenantId}}/wizard/{zaman damgası}_{Ad}.json` +9. **Doğrulama ve geri alma** notları + +**Adım bileşenleri.** Adımlar numaralı dosya adları yerine işlevleriyle adlandırılır ve +`React.lazy` ile yüklenir; aynı anda yalnızca görünen adım indirilir, sihirbazın açılış maliyeti +adım sayısından bağımsızdır. + +| Adım | Dosya | İçerik | +| --- | --- | --- | +| Menü ve kimlik | `WizardStepMenu.tsx` | Wizard adı, ListForm kodu, menü ağacı/üst menü, ikon, sıra, izin grubu, dil metinleri | +| Veri ayarları | `WizardStepDataSettings.tsx` | Veri kaynağı, select komutu tipi, anahtar alan, CRUD endpoint diyaloğu | +| Alanlar | `WizardStepFields.tsx` | Sütun grupları, editör tipleri, lookup, doğrulama | +| Alt formlar | `WizardStepSubForms.tsx` | Ana–detay ilişki eşlemesi | +| Widget'lar | `WizardStepWidgets.tsx` | KPI kartları | +| İş akışı | `WizardStepWorkflow.tsx` | Onay/koşul grafiği | +| Düzen adımları | `WizardStepTodoLayout` · `WizardStepTreeLayout` · `WizardStepGanttLayout` · `WizardStepSchedulerLayout` | Yalnızca ilgili görünüm açıksa görünür | +| Bileşen | `WizardStepComponent.tsx` · `WizardStepCustomComponent.tsx` | Custom yolunda bağlanacak bileşen | +| Yayınlama | `WizardStepDeploy.tsx` | Özet, doğrulama ve seed dosyasının üretimi | + +Menü ağacı yardımcıları (`menuTree.ts`) adım bileşenlerinden ayrı bir modüldedir; böylece Wizard +ve `SqlTableDesignerDialog` bu fonksiyonları kullanırken adım bileşenlerini pakete çekmez. + +**İki yol: `ComponentKind`.** `ListFormWizardDto.ComponentKind` sihirbazın yol ayrımıdır ve seed +dosyasında ilk alandır. Varsayılan `List` olduğu için bu alanı taşımayan eski seed dosyaları +eskisi gibi çalışır. + +| `ComponentKind` | Üretilen | Menü URL'i | Yetkiler | +| --- | --- | --- | --- | +| `List` | ListForm + ListFormField + Route + Menu + Permission + dil anahtarları | `/admin/list/{ListFormCode}` | `.Create`, `.Update`, `.Delete`, `.Export`, `.Import`, `.Note` | +| `Custom` | Menü + Permission + dil anahtarları; ekran seçilen Custom Component'tir | Component'in `RoutePath` değeri | Yalnızca `.Create`, `.Update`, `.Delete` (Export/Import/Note ListForm'a özgüdür) | + +**Menüsüz wizard (`CreateMenu = false`).** Menü (ve gerekiyorsa üst menü) kaydı hiç üretilmez; +ListForm, permission ve dil anahtarları her iki durumda da üretilir. Menüsüz wizard'lar başka bir +ekranda SubGrid/parça olarak kullanılmak üzere tanımlanır. Alan varsayılanı `true` olduğundan bu +alanı taşımayan eski seed dosyaları etkilenmez. + +**İzin grubu seçimi.** Menü adımında izin grubu bir seçim listesinden gelir +(`GetWizardPermissionGroups`). Görünen adlar, grup adıyla aynı olan dil anahtarının metinlerinden +okunur; seçim yapıldığında `PermissionGroupDisplayNameEn/Tr` forma doldurulur. Yeni bir grup +tanımlanırsa grup adıyla aynı dil anahtarına bu metinler yazılır; mevcut grup seçilip alanlar boş +bırakılırsa sunucu veritabanındaki değeri geri yazar. Böylece seed dosyası her durumda iki dilli +görünen adı taşır. Dinamik liste ekranlarının route deseni `/admin/list/{ListFormCode}` biçimindedir; form görünümleri `/admin/form/{listFormCode}/:id`, grafik `/admin/chart/{listFormCode}`, pivot @@ -429,13 +538,49 @@ Var olan bir ekranı düzenlemek için `/admin/listform/edit/{listFormCode}` — yapılandırması sekmeli bir editörde yönetilir (Veritabanı, Sütunlar, Filtreler, Düzenleme, Yetkiler, Alt Form, Pivot, Tree, Gantt, Scheduler, Todo, Widget, Workflow, Durum, Grafik sekmeleri). +**Wizard File Manager** (`/admin/listform/wizard` liste ekranı) üretilmiş seed dosyalarını yönetir: +liste/kart görünümü arasında geçiş (tercih diğer listelerle aynı yerde, `admin.lists.states` +altında saklanır), arama, düzenleme (dosyanın yerine geçen yeni bir çalıştırma), silme, veritabanı +migrate tetikleme, dışa aktarma ve içe aktarma. Butonlar `App.Listforms.Wizard.*` yetkileriyle +gizlenir; asıl kontrol `ListFormWizardAppService` üzerindedir. + ### 7.7. İçe/dışa aktarma +**Veri (ekran içi)** + - **Dışa aktarma:** xlsx, csv, pdf (grid durumuna ve görünür sütunlara saygı duyar). - **İçe aktarma:** `components/importManager` altındaki dosya yükleme → önizleme → ilerleme → sonuç akışı; sonuçlar `ListFormImportLog` üzerinde satır bazında raporlanır. Şablon dosyası ekran tanımından üretilir. +**Ekran tanımı (wizard seed paketi)** + +Bir ekranın tanımı, bağımlılıklarıyla birlikte tek bir zip olarak taşınır. Zip içindeki yollar seed +kapsam klasörü (`host` / `tenants/{tenantId}`) kökü baz alınarak yazılır; arşiv başka bir ortamda +aynı klasör düzenine doğrudan açılabilir. + +| Yol | İçerik | +| --- | --- | +| `wizard/{dosya}.json` | Wizard tanımının kendisi | +| `custom/{component}.json` | Custom yolunda bağlı component ve bağımlılıkları | +| `crud/{entity}.json` | Component'lerin/nesnenin kullandığı CRUD endpoint tanımları | +| `{sql\|postgres}/{object\|execute}/{nesne}.sql` | List yolunda `SelectCommand`'a karşılık gelen SQL nesnesi | + +**İçe aktarma iki adımlıdır** (`WizardImportDialog`): + +1. **Analiz** (`AnalyzeImport`) — zip, seed kökü altındaki `.imports/{importId}/staged` klasörüne + açılır (nokta ile başladığı için seeder taramalarına girmez) ve her dosya hedefteki karşılığıyla + karşılaştırılır: `New` (hedefte yok), `Identical` (aynı, sorulmaz), `Conflict` (farklı). + Çakışan dosyalar Monaco `DiffEditor` ile karşılaştırılıp birleştirilir. +2. **Uygulama** (`ApplyImport`) — dosyalar tek tek yazılır, ilerleme satır satır gösterilir. Yazılan + her dosyanın önceki hâli `.imports/{importId}/backup` altına alındığı için hata durumunda + `RollbackImport` ile toplu işlem geri alınabilir; `CompleteImport` oturumu kapatır. + +Güvenlik sınırları: yalnızca `wizard`, `crud`, `custom`, `sql`, `postgres` kök klasörleri +(SQL sağlayıcı klasörlerinin altında yalnızca `object` ve `execute`) kabul edilir; dosya başına +5 MB, arşiv başına 50 MB, en çok 500 girdi. Kabul edilmeyen girdiler analiz sonucunda uyarı +olarak listelenir. Yetkiler: `App.Listforms.Wizard.Export` / `App.Listforms.Wizard.Import`. + --- ## 8. Developer Kit @@ -444,12 +589,40 @@ Teknik kullanıcılar için `/admin/developerkit` altında toplanan araçlar: | Araç | Route | Ne işe yarar | | --- | --- | --- | -| **SQL Query Manager** | `/admin/sql-manager` | Nesne gezgini, Monaco tabanlı SQL editörü, sonuç grid'i, **tablo tasarımcısı** ve **CRUD endpoint yönetimi**. | +| **SQL Query Manager** | `/admin/sqlQueryManager` | Nesne gezgini, Monaco tabanlı SQL editörü, sonuç grid'i, **tablo tasarımcısı**, **view tasarımcısı** ve **CRUD endpoint yönetimi**. | | **Custom Endpoint** | `/admin/list/App.DeveloperKit.CustomEndpoints` | SQL veya servis tanımından REST endpoint üretimi; GET/POST/PUT/DELETE için ayrı yetki kapıları (`App.DeveloperKit.CustomEndpoints.*`) ve kullanıcı/rol/global erişim kuralları. | | **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. | -| **Custom Component** | `/admin/developerkit/components` | React bileşenini veritabanında saklama; `@babel/standalone` ile tarayıcıda derleyip route'a bağlama. | +| **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. | +| **Wizard Manager** | `/admin/listform/wizardManager` | Üretilmiş wizard seed dosyalarının listesi/kartı, düzenleme, silme, DB migrate, export/import. Yetki `App.Listforms.Wizard{,.Create,.Update,.Delete,.Export,.Import}`. | + +> Bu ekranların artefakt şemaları, enum değerleri ve çalışan örnekleri +> [`.github/instructions/lowcode.instructions.md`](.github/instructions/lowcode.instructions.md) +> dosyasındadır. + +### 8.1. SQL View Designer + +`SqlViewDesignerDialog` (+ `sqlViewDesigner/`), SSMS Query Designer düzeninde görsel view +oluşturma aracıdır. Üç panel: + +- **Diagram** (`DiagramPane.tsx`) — `ViewSource` kutuları (x/y konumlu, daraltılabilir, yalnızca + seçili kolonları gösterebilir) ve aralarındaki JOIN okları. Kaynak bir tablo/view olabileceği + gibi `CROSS/OUTER APPLY` ya da türetilmiş bir alt sorgu da olabilir; alt sorgunun gövdesi + olduğu gibi saklanır. JOIN türleri: `INNER`, `LEFT`, `RIGHT`, `FULL`, `CROSS`; koşul + operatörleri `=`, `<>`, `>`, `>=`, `<`, `<=`. +- **Criteria grid** (`CriteriaGrid.tsx`) — Column / Alias / Output / Group By / Sort / Filter / + Or… satırları. Bir satır ya `sourceId + columnName` referansıdır ya da serbest bir SQL + ifadesidir. Group By sütunu `GroupBy`, `Where` (satır çıktıya girmez, yalnızca filtre taşır) ve + `SUM`/`COUNT`/`COUNT_DISTINCT`/`AVG`/`MIN`/`MAX` değerlerini alır. Filtre hücreleri SSMS'teki + gibi serbest yüklemdir (`> 100`, `LIKE '%abc%'`, `IS NULL`). +- **T-SQL önizleme** — model → SQL üretimi (`generateViewSql`). + +Tasarımcı tek yönlü çalışır: **model → T-SQL**. Mevcut bir view açılırken `parseViewSql` ile geri +okunmaya çalışılır; üretilen kanonik şekle uymayan bir tanım gelirse dialog **ham SQL moduna** +düşer ve kullanıcı Query Editor'dan devam eder (`onOpenInEditor`). PostgreSQL bağlantılarında +`isPostgreSql` bayrağı ile söz dizimi buna göre üretilir. > **CRUD Endpoint, SQL Query Manager ile birleştirildi.** Ayrı `/admin/developerkit/endpoints` > ekranı ve menüsü kaldırıldı. Endpoint üretme, aktif/pasif etme, test etme ve silme işlemleri @@ -470,6 +643,67 @@ Teknik kullanıcılar için `/admin/developerkit` altında toplanan araçlar: > `LastModifierId`, `IsDeleted`, `DeletionTime`, `DeleterId`. Kullanıcı açıkça istemedikçe bu > kolonlar çıkarılmaz. +### 8.2. Custom Component ve Visual Designer + +**Saklanan tanım.** Bir custom component `Name`, `RoutePath`, `Code` (JSX), `Props`, +`Dependencies` ve `DataSources` alanlarından oluşur. Görsel tasarımcı dokümanı iki yerde tutulur: +`Props.visualDesigner` içinde (nodes, canvas, lifecycle, dataSources) ve üretilen kodun ilk +satırındaki `/*__SOZSOFT_VISUAL_DESIGNER__…__*/` yorumunda. Böylece yalnızca kod elinizdeyse bile +tasarımcı dokümanı geri okunabilir. `sourceMode` alanı `visual` ya da `code` olur; kod moduna +geçilen bir bileşen kanvasa geri dönmez. + +**Seed senkronizasyonu.** Component Manager üzerinden kaydedilen/silinen her bileşen +`configs/seeds/{host|tenants/{tenantId}}/custom/{Name}.json` olarak yazılır; +`CustomComponentDataSeeder` veritabanı silinip yeniden oluşturulduğunda aynı dosyaları okuyup geri +yükler. Dosya düzeni bilinçli olarak `TenantData.json` içindeki `CustomComponents` bloğuyla +aynıdır. `DataSources` sütunu tasarımcı dokümanından türetilir +(`CustomComponentDataSourceResolver`): Data sekmesindeki her endpoint `method + path` ile CRUD +endpoint kataloğunda aranır, eşleşenler `EntityName` + `crud/{EntityName}.json` referansını taşır, +elle yazılmış olanlar listede kalır ama bu alanları boş gelir. Aynı çözüm hem kaydetmede (katalog +veritabanından) hem seed'de (katalog `crud/*.json` dosyalarından) kullanıldığı için sütun iki +yolda da aynı üretilir. + +**Toolbox.** Kanvasa bırakılabilecekler beş aileye ayrılır: + +| Aile | İçerik | +| --- | --- | +| `layout` | `PageContainer`, `FlexRow` (kolon sayısı, ilk kolon genişliği, hizalama, gap, wrap), `Spacer` | +| `data` | `Form` — ASP.NET'in Form + FormView karşılığı; dört CRUD endpoint'ini sahiplenen kapsayıcı | +| `platform` | `ListView`, `DataGridView`, `TreeView`, `GanttView`, `TodoBoard`, `CardView`, `SchedulerView`, `PivotView`, `ChartView` — hepsi `listFormCode` ile bir ListForm ekranını gömer | +| `html` / `ui` | Ham HTML etiketleri ve `components/ui` tasarım sistemi bileşenleri (sözleşmeleri metadata'dan okunur) | +| `custom` | Başka custom component'ler (bağımlılık olarak kaydedilir) | + +**Form bileşeni.** `selectEndpoint` / `insertEndpoint` / `updateEndpoint` / `deleteEndpoint`, +`keyFieldName`, `collectionPath`, `keySource` + `keyParamName`, `previewKeyValue`, `autoLoad`, +`showToolbar`, `columnCount`, `gap` özellikleriyle yapılandırılır. İçine bırakılan her bileşen +Select sonucunun bir kolonuna bağlanır ve Save/Delete üzerinden geri yazar. + +**Veri bağlama.** `DesignerBinding` bir kaynağa (`sourceId`) ve yola (`path`) bağlanır; `labelPath` +/ `valuePath` seçim bileşenleri içindir. `columns` ile ekranda görünmeyen ek sütunlar da taşınır +ve script'ten `refs..getColumn('sutun')` ile okunup başka bir bileşenin değerine ya da Form +kaydına yazılır. Filtreler CrudEndpoint `GetList` sözleşmesine göre üretilir: `eq` çıplak query +parametresi (`?RoleId=…`), diğerleri son ek taşır (`?Name.contains=…`). Operatörler: `eq`, `ne`, +`contains`, `startswith`, `endswith`, `gt`, `gte`, `lt`, `lte`, `in`, `isnull`, `notnull`. + +**Yetki modeli.** İki katmanlıdır: + +- **Node görünürlüğü** — `designerPermission` prop'u dolu olan bir düğüm, yetki verilmemişse + render edilmez. +- **Form komutları** — her komut (`selectPermission`, `insertPermission`, `updatePermission`, + `deletePermission`) ya serbesttir (boş) ya da bir yetkiye bağlıdır. `Otomatik` modda temel, + Wizard'ın bu bileşen için ürettiği okuma yetkisidir (menü kaydının korunduğu yetkinin aynısı) ve + komutlara `''` / `.Create` / `.Update` / `.Delete` son ekleri eklenir. `Özel` modda yetki adı + elle yazılır. + +**Script.** Tasarımcı, ListForm Editor Script ile aynı ortak dialog'u kullanır +(`designerScriptDialect.ts` + `designerScriptRecipes.ts`); tarif grupları bileşen erişimi, API +çağrıları, form ve olay/sayfa başlıkları altında toplanır. + +**Diğer notlar.** `PlatformIcon` tasarımcı dokümanındaki ikon adını çözer; `selectComponents.ts` +`Select.componentAs` için saklanan adı (`ReactSelect`, `CreatableSelect`, `AsyncSelect`) gerçek +bileşene çevirir — kanvas ve üretilen bileşenin aynı adı aynı şekilde yorumlamasını sağlayan tek +nokta budur. Yeni bileşen `configs/seeds/host/custom/NewComponent.json` şablonundan başlar. + **Dinamik servis yaşam döngüsü:** `DynamicServiceCompiler` kodu derler → `DynamicAssemblyRegistrationService` assembly'yi tenant bağlamıyla kaydeder → `ActionDescriptorChangeProvider` MVC'ye route tablosunun değiştiğini bildirir → @@ -503,6 +737,19 @@ Route.Authority ──┘ └► Permission `App.Orders.*`, `App.BlogManagement.*`, `App.IdentityManagement.*`, `App.Reports.*`, `App.Administration`, `App.Setting`, `App.Setup.Migrate`. +Aksiyon yetkisi olan alt gruplar (UI tarafındaki karşılıkları `constants/permission.constant.ts` +içindedir; oradaki kontroller yalnızca butonları gizler, asıl kontrol AppService'lerdedir): + +| Grup | Alt yetkiler | +| --- | --- | +| `App.Listforms.Wizard` | `.Create`, `.Update`, `.Delete`, `.Export`, `.Import` | +| `App.DeveloperKit.Components` | `.Create`, `.Update`, `.Delete` | +| `App.DeveloperKit.CustomEndpoints` | `.Get`, `.Post`, `.Put`, `.Remove` (dispatcher üzerinden çağrı kapısı; endpoint bazlı User/Role/Global kuralları bunun üstünde denetlenir) | +| `App.DeveloperKit.DynamicServices` | `.Create`, `.Edit`, `.Delete`, `.Manage`, `.TestCompile`, `.Publish`, `.ViewCode` | +| `App.SqlQueryManager` | `.CrudEndpoints` | +| `App.Setup.Migrate` | Migration + seed tetikleme (host tarafı) | +| `App.{Home,About,Services,Contact}.Design` | Public site sayfa tasarım modu (`?design=1`) | + > **Kural:** Yetki sözleşmesi olmayan menü/route önerilmez ve eklenmez. --- @@ -729,6 +976,9 @@ konfigürasyonu ile üretilir. - Tailwind tabanlı tasarım sistemi (`components/ui`) + DevExtreme temaları; açık/koyu mod ve tema yapılandırıcı (`ThemeConfigurator`). +- **Varsayılan mod koyudur.** `proxy/theme/theme.config.ts`: `mode` ve `navMode` `dark`, DevExtreme + stili `dx.material.blue.dark.compact`. Kullanıcı tercihi store üzerinden saklanır; bu yalnızca + ilk açılış varsayılanıdır. - Dil metinleri veritabanından gelir (`Sozsoft.Languages`); dil seçici header'dadır. Tenant bazlı çeviri geçersiz kılma desteklenir. - Layout seçenekleri: dikey menü, yatay menü, yığılmış yan menü, mobil navigasyon. @@ -828,6 +1078,11 @@ Script sırası (`configs/deployment/scripts`): 8-dev-build.sh / 9-dev-deploy.sh / 10-dev-migrator-true.sh → Dev ortamı ``` +Migrator servisi `SEED` ortam değişkeni ile çalışır ve compose dosyasında `SEED=${SEED:-true}` +olarak tanımlıdır: değişken verilmezse seed **çalışır**. Seed dosyaları imaja gömülmez; depodaki +`configs/seeds` klasöründen mount edilir (`App:SeedsPath`), böylece bir seed değiştiğinde imaj +build etmek gerekmez. + Ayrıntılı kurulum notları: [`configs/deployment/README.md`](configs/deployment/README.md), [`Readme_Production.md`](configs/deployment/Readme_Production.md), [`Readme_Devops.md`](configs/deployment/Readme_Devops.md). @@ -867,25 +1122,61 @@ dotnet build dotnet run --project src/Sozsoft.Platform.HttpApi.Host dotnet format --include .\modules\Sozsoft.Notifications\ --folder +# Kod standardı kapısı (ölü kod / stil analizörleri) +dotnet build Sozsoft.Platform.sln --no-incremental ` + -p:EnforceCodeStyleInBuild=true -p:GenerateDocumentationFile=true +dotnet format Sozsoft.Platform.sln --diagnostics IDE0005,IDE0161 --severity warn + # Migration (EntityFrameworkCore projesi içinde) dotnet ef migrations add dotnet ef database update dotnet ef migrations remove ``` +.NET/ABP kod standardı (hedef sürümler, nullable politikası, ölü kod kuralları, modern C# +kullanımı, ABP katman sınırları) bağlayıcı olarak +[`.github/instructions/dotnet.instructions.md`](.github/instructions/dotnet.instructions.md) +dosyasındadır. Standart derlemede zorlanır: `api/Directory.Build.props` analizörleri açar, +`api/.editorconfig` kural şiddetlerini tanımlar; bu iki dosya standardın tek kaynağıdır. + Yeni ABP modülü eklemek için: `abp new Sozsoft. -t module --no-ui -m none --database-provider ef` ### 21.4. Seed dosyaları -Yeni bir modülün nasıl kurulacağını öğrenmek için başvurulacak dosyalar: +İki ayrı seed yüzeyi vardır ve karıştırılmamalıdır. -- `api/src/Sozsoft.Platform.DbMigrator/Migrations/ListFormSeeder_Saas.cs` -- `.../Migrations/ListFormSeeder_Administration.cs` -- `.../Migrations/MenusData.json` -- `.../Migrations/PermissionsData.json` -- `.../Migrations/HostData.json` -- `.../Migrations/LanguagesData.json` +**1. Depo içi (kod ile taşınan) seed'ler** — `api/src/Sozsoft.Platform.DbMigrator/Migrations/`. +Platformun kendi ekranları, menüleri, yetkileri ve dil metinleri buradadır; değişiklikleri +derlenip yayınlanır. Yeni bir modülün nasıl kurulacağını öğrenmek için başvurulacak dosyalar: + +- `ListFormSeeder_Saas.cs`, `ListFormSeeder_Administration.cs` — ListForm + alan tanımları +- `MenusData.json` — `Routes`, `MenuGroups`, `Menus` +- `PermissionsData.json` — yetki grupları ve tanımları +- `HostData.json` — ayar/entegrasyon değerleri +- `LanguagesData.json` — dil anahtarları (EN/TR) +- `WizardDataSeeder.cs`, `CustomComponentDataSeeder.cs`, `CrudDataSeeder.cs` — aşağıdaki + runtime seed dosyalarını okuyup uygulayan seeder'lar + +**2. Runtime'da üretilen seed'ler** — `configs/seeds/`. Wizard, Component Manager ve CRUD Endpoint +Manager çalışma zamanında burayı yazar; veritabanı silinip yeniden oluşturulduğunda aynı dosyalar +geri yüklenir. Kapsam klasörü CDN düzeniyle aynıdır ve `SeedPathResolver` üzerinden çözülür: + +``` +configs/seeds/ +├── host/ # host kapsamı +│ ├── wizard/ # {zaman damgası}_{Ad}.json → WizardDataSeeder +│ ├── custom/ # {ComponentName}.json → CustomComponentDataSeeder +│ ├── crud/ # {EntityName}.json → CrudDataSeeder +│ ├── sql/{object,execute}/ # .sql (SQL Server) +│ └── postgres/{object,execute}/ +├── tenants/{tenantId}/… # aynı düzen, tenant kapsamı +└── .imports/{importId}/ # wizard import staging + backup (seeder taramalarına girmez) +``` + +> Bu dosyalar elle de düzenlenebilir; ilgili ekrandan tekrar kaydedildiğinde yeniden üretilirler. +> Wizard dosyası `EditFileName` ile güncellenirse sunucu önce eski dosyayı ve ürettiği kayıtları +> siler, sonra yenisini üretir. --- @@ -913,8 +1204,11 @@ Yeni bir modülün nasıl kurulacağını öğrenmek için başvurulacak dosyala - `Telegram` bildirim kanalı altyapıda tanımlıdır ancak UI'da kapalıdır. - Depodaki `appsettings*.json` ve deployment dokümanları geliştirme değerleri/örnek kimlik bilgileri içerir; üretim öncesi tümü değiştirilmelidir. -- `.github/instructions/ai.instructions.md` içinde stack ".NET 9 + ABP 9" olarak yazılıdır; - kod tabanı **.NET 10 + ABP 10** üzerindedir. Kural bölümleri geçerliliğini korur. +- SQL View Designer tek yönlüdür (model → T-SQL). Elle yazılmış ya da tasarımcının kanonik + şekline uymayan bir view tanımı geri okunamaz; dialog ham SQL moduna düşer. +- `api/test/Sozsoft.Platform.EntityFrameworkCore.Tests` şu an SQLite şema oluşturmada + (`nvarchar(max)` → `SQLite Error 1`) kırıktır; bu kırıklık kod standardından önce de vardı ve + ayrı bir iş olarak ele alınmalıdır. - Rol–yetki eşleşmeleri veritabanında müşteri ortamına göre değişir; buradaki yetki kodları tanımın kendisidir, atama değil. diff --git a/claude.md b/claude.md index d3b31dda..cdd5a0f3 100644 --- a/claude.md +++ b/claude.md @@ -92,3 +92,79 @@ and drop the rest. An empty or obvious heading is noise. 7. Tenant isolation notes 8. Validation and test checklist 9. Rollback strategy + +--- + +## Standing Context (read this before proposing anything) + +These sections encode expectations the user will **not** repeat in every prompt. +`README.md` is the technical map of the platform; this file is how to act on it. + +### Where the truth lives + +| Konu | Kaynak | +| --- | --- | +| Platform davranışı, karar sırası, çıktı sözleşmesi | `.github/instructions/ai.instructions.md` | +| **Artefakt üretimi**: seed şemaları, enum değerleri, çalışan örnekler | `.github/instructions/lowcode.instructions.md` | +| `api/` kod standardı (sürüm, nullable, ölü kod, modern C#, ABP katmanları) | `.github/instructions/dotnet.instructions.md` | +| Modül/liste ekleme prosedürü, seeder dosya sorumlulukları | `.github/instructions/list.instructions.md` | +| Ne nerede yaşıyor, hangi ekran ne yapıyor | `README.md` | +| Yetki kodları | `PlatformConsts.AppCodes` (backend) + `constants/permission.constant.ts` (UI) | +| Seed yolları | `SeedPathResolver` | + +Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` → bu dosya → `README.md`. + +### Standing defaults — do not ask again + +1. **Yeni tablo** → tenant + tam audit kolonları varsayılan gelir (`Id`, `TenantId`, + `CreationTime`, `CreatorId`, `LastModificationTime`, `LastModifierId`, `IsDeleted`, + `DeletionTime`, `DeleterId`). Kullanıcı açıkça "tenant yok / audit yok" demedikçe çıkarma. +2. **Yeni ekran** → Wizard yolu. `Definitions` altına otomatik yerleştirme yok; modüle ait yeni + bir kök menü oluştur, `Order = max(Order) + 1`. +3. **Yeni ekran/menü** → yetki sözleşmesi olmadan önerilmez. Menü ve route aynı ekran + sözleşmesini göstermeli. +4. **Kullanıcıya görünen her metin** → dil anahtarı üzerinden, EN + TR birlikte. Koda gömülü + metin önerme. +5. **Runtime'da üretilen her artefakt** (wizard, custom component, crud endpoint) → ilgili seed + dosyası da yazılır/güncellenir. Veritabanı sıfırlandığında geri gelmeyen bir şey üretme. +6. **Ekran/komponent talebi — yazılı ya da görsel** → önce ekran tarifi çıkar, sonra yolu seç + (grid benzeri iş → SQL Query Manager + Wizard; serbest yerleşim → Custom Component), + sonra `configs/seeds/` altına dosyaları üret. Şüphede Wizard. Ekran görüntüsü piksel + sözleşmesi değildir; platformun kendi bileşenleriyle en yakın karşılık üretilir. + Ayrıntı: `lowcode.instructions.md` §0. +7. **Ekran içi hesap/koşul ihtiyacı** → önce `EditorScript` (Script Builder tarifleri), sonra + `EditorOptions`; kod yazmak son çare. +8. **SQL** → her zaman parametreli. String birleştirme ile sorgu kurma. +9. **Silme** → `DeleteCommand = DefaultDeleteCommand("{Tablo}")`; ham SQL string'i yazma. + +### Cross-cutting: touching one thing means touching these too + +- **`scriptRecipes.ts` (TS) ↔ `Domain.Shared/Editors/*.cs`** — biri değişirse diğeri de değişir. + İkisinin çıktısı **birebir aynı** olmak zorundadır; aksi hâlde seeder ile basılan script + dialogda "elle düzenlenmiş" sayılır ve kural editörü kapanır. +- **Yeni bir yetki** → `PlatformConsts.AppCodes` + `PermissionsData.json` + (UI'da kullanılacaksa) + `permission.constant.ts` + `LanguagesData.json`. +- **Yeni bir dil anahtarı** → `LanguagesData.json`; eklemeden önce anahtarın zaten var olup + olmadığını kontrol et. +- **Custom component kaydı/silinmesi** → `custom/{Name}.json`; kullandığı endpoint'ler + `crud/{Entity}.json`. +- **Wizard dosyası** → tek başına taşınmaz; export zip'i `wizard/`, `custom/`, `crud/` ve + `{sql|postgres}/{object|execute}/` bağımlılıklarını da içerir. +- **`MenusData.json`** → `Routes` bölümüne dokunma; yalnızca `MenuGroups` ve `Menus`. + +### Definition of done + +- Ölü kod bırakma: kullanılmayan `using`, private üye, alan, DTO, hook, servis, tip. Bir tip + silinmeden önce **repo geneli** (api + ui + configs) referans taraması yapılır; DI ile çözülen + tipler düz metin aramasında referanssız görünür. +- Yorum satırına alınmış kod bloğu bırakılmaz. +- `api/` değişikliğinden sonra: `dotnet build -p:EnforceCodeStyleInBuild=true` — 0 hata, yeni + `IDE00xx` yok. +- `ui/` değişikliğinden sonra: `npm run typecheck` ve `npm run lint`. +- Yorumlar **neden**i anlatır, ne yaptığını değil. Kod zaten ne yaptığını söylüyor. + +### Tone + +- Türkçe soruya Türkçe cevap. +- Platformu tanıtma, mimariyi özetleme, ne yapacağını anlatıp sonra bir de ne yaptığını özetleme. +- Cevabın boyutu sorunun boyutu kadar olsun. diff --git a/ui/src/constants/actionButton.constant.ts b/ui/src/constants/actionButton.constant.ts new file mode 100644 index 00000000..23dcf853 --- /dev/null +++ b/ui/src/constants/actionButton.constant.ts @@ -0,0 +1,12 @@ +/** Liste/kart yonetim ekranlarindaki aksiyon ikonlari notr durur, rengi yalnizca hover'da gosterir. */ +export const ACTION_BUTTON_CLASS = + '!h-8 !w-8 !rounded !p-0 text-slate-500 transition-colors dark:text-gray-400' + +export const ACTION_BUTTON_HOVER = { + blue: 'hover:!bg-blue-50 hover:text-blue-600 dark:hover:!bg-gray-800 dark:hover:text-blue-400', + green: + 'hover:!bg-green-50 hover:text-green-600 dark:hover:!bg-gray-800 dark:hover:text-green-400', + red: 'hover:!bg-red-50 hover:text-red-600 dark:hover:!bg-gray-800 dark:hover:text-red-400', + slate: + 'hover:!bg-slate-100 hover:text-slate-700 dark:hover:!bg-gray-800 dark:hover:text-gray-200', +} as const diff --git a/ui/src/sw.ts b/ui/src/sw.ts index f5c6d9d4..c31c937c 100644 --- a/ui/src/sw.ts +++ b/ui/src/sw.ts @@ -37,7 +37,7 @@ declare const self: { __WB_MANIFEST: PrecacheEntry[] location: Location navigator?: { connection?: { saveData?: boolean; effectiveType?: string } } - registration: { scope: string } + registration: { scope: string; installing?: unknown; waiting?: unknown } skipWaiting: () => Promise clients: { claim: () => Promise @@ -133,6 +133,16 @@ async function fetchWarmupList(): Promise { let warmupPromise: Promise | undefined const loadWarmupList = () => (warmupPromise ??= fetchWarmupList()) +/** + * Yeni bir sürüm kuruluyor ya da geçişi bekliyor mu? + * + * Bu kontrol ISITMA için: eski aktif worker ısıtmayı sürdürürken yenisi kurulum + * yapıyorsa iki oturum hem bant genişliği için yarışır hem de istemcide iç içe + * geçmiş iki ilerleme akışı üretir. Kurulum önceliklidir. (Kurulum oturumunun + * kendisi bu kontrolü kullanamaz: kuran worker `installing`'in ta kendisidir.) + */ +const updatePending = () => !!(self.registration.installing || self.registration.waiting) + /** Veri tasarrufu açıkken ya da 2G'de ~20 MB'lık ısıtma yapılmaz. */ function isWarmupAllowed() { const connection = self.navigator?.connection @@ -367,14 +377,20 @@ async function runSession({ files, jobs, criticalCount, reusedCount, phase }: Se }) } + // Isıtma, araya bir kurulum girdiğinde kalan işleri bırakır (bkz. + // `updatePending`); kurulumun kendisi hiçbir koşulda kesilmez. + const abandoned = () => phase === 'warmup' && updatePending() + const run = async (pool: DownloadJob[], concurrency: number, deadline?: number) => { let cursor = 0 const worker = async () => { for (;;) { const next = cursor++ if (next >= pool.length) return - // Süre bütçesi dolduysa kalan bileşenler runtime cache'e bırakılır. + // Süre bütçesi dolduysa ya da kurulum başladıysa kalan bileşenler + // runtime cache'e bırakılır. if (deadline && Date.now() > deadline) return + if (abandoned()) return const job = pool[next] const file = files[job.index] @@ -453,7 +469,8 @@ async function installAssets() { let warmupRunning = false async function ensureWarmup() { - if (warmupRunning || !isWarmupAllowed()) return + // Kurulum sürüyorsa bileşenler zaten o oturumda iniyor; ikinci akış açma. + if (warmupRunning || updatePending() || !isWarmupAllowed()) return warmupRunning = true try { const assetCache = await caches.open(ASSET_CACHE) diff --git a/ui/src/views/admin/listForm/wizard/WizardFileManager.tsx b/ui/src/views/admin/listForm/wizard/WizardFileManager.tsx index 63489805..6bfca1ee 100644 --- a/ui/src/views/admin/listForm/wizard/WizardFileManager.tsx +++ b/ui/src/views/admin/listForm/wizard/WizardFileManager.tsx @@ -37,6 +37,7 @@ import { WizardComponentKindEnum, WizardFileInfoDto } from '@/proxy/admin/wizard import { useNavigationIcons } from '@/proxy/menus/navigation-icon.config' import { usePermission } from '@/utils/hooks/usePermission' import { WIZARD_PERMISSION } from '@/constants/permission.constant' +import { ACTION_BUTTON_CLASS, ACTION_BUTTON_HOVER } from '@/constants/actionButton.constant' import WizardImportDialog from './WizardImportDialog' /** Layout tercihi diger listelerle ayni yerde (admin.lists.states) saklanir. */ @@ -178,55 +179,55 @@ const WizardFileManager = () => { {canUpdate && ( + /> )} {/* Menünün açtığı adres yeni sekmede açılır; adres yoksa buton anlamsızdır. */} {f.menuUrl && ( + /> )} {canExport && ( + /> )} {canDelete && ( + /> )} ) diff --git a/ui/src/views/developerKit/ComponentManager.tsx b/ui/src/views/developerKit/ComponentManager.tsx index 683aecc8..0f4d9186 100644 --- a/ui/src/views/developerKit/ComponentManager.tsx +++ b/ui/src/views/developerKit/ComponentManager.tsx @@ -31,6 +31,7 @@ import Select from '@/components/ui/Select' import type { CustomComponent } from '@/proxy/developerKit/models' import { useStoreActions, useStoreState } from '@/store/store' import type { ListViewLayoutType } from '../admin/listForm/edit/types' +import { ACTION_BUTTON_CLASS, ACTION_BUTTON_HOVER } from '@/constants/actionButton.constant' /** Layout tercihi diger listelerle ayni yerde (admin.lists.states) saklanir. */ const VIEW_STATE_CODE = 'developerkit-components' @@ -146,53 +147,53 @@ const ComponentManager: React.FC = () => { {canUpdate && ( + /> )} + /> + /> {canDelete && ( + /> )} ) diff --git a/ui/src/views/developerKit/DynamicServiceManager.tsx b/ui/src/views/developerKit/DynamicServiceManager.tsx index 2445b14e..c04eb609 100644 --- a/ui/src/views/developerKit/DynamicServiceManager.tsx +++ b/ui/src/views/developerKit/DynamicServiceManager.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from 'react' -import { Link } from 'react-router-dom' +import { Link, useNavigate } from 'react-router-dom' import { FaPlus, FaSearch, @@ -10,6 +10,8 @@ import { FaCode, FaSpinner, FaExternalLinkAlt, + FaTh, + FaList, } from 'react-icons/fa' import Widget from '@/components/common/Widget' import { useLocalization } from '@/utils/hooks/useLocalization' @@ -19,9 +21,16 @@ 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') }, @@ -37,6 +46,13 @@ const DynamicServiceManager: React.FC = () => { 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() }, []) @@ -79,15 +95,53 @@ const DynamicServiceManager: React.FC = () => { }) const statusBadge = (status: string) => { - if (status === 'Success') return 'bg-emerald-100 text-emerald-700' - if (status === 'Failed') return 'bg-red-100 text-red-700' - return 'bg-yellow-100 text-yellow-700' + 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) => ( + + {service.compilationStatus} · v{service.version} + + ) + + const renderActions = (service: DynamicServiceDto) => ( +
+
+ ) + return (
@@ -145,8 +199,8 @@ const DynamicServiceManager: React.FC = () => { type="text" placeholder={translate('::App.DeveloperKitDynamicServices.SearchPlaceholder')} value={searchTerm} - onChange={(e) => setSearchTerm(e.target.value)} 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)} />
@@ -161,11 +215,11 @@ const DynamicServiceManager: React.FC = () => {
@@ -179,6 +233,22 @@ const DynamicServiceManager: React.FC = () => { {translate('::App.DeveloperKitDynamicServices.NewService')}
+
+
{/* List */} @@ -187,79 +257,138 @@ const DynamicServiceManager: React.FC = () => { ) : filteredServices.length > 0 ? ( -
- {filteredServices.map((service) => ( -
-
-
-
-
-

- {service.name} -

-
+ viewMode === 'card' ? ( +
+ {filteredServices.map((service) => ( +
+
+
+ {/* Sol taraf */} +
+
+

+ {service.name} +

+
+
+ + {service.displayName && ( +

+ {service.displayName} +

+ )} + +
{renderStatusBadge(service)}
+ + {service.description && ( +

+ {service.description} +

+ )}
- {service.displayName && ( -

- {service.displayName} -

- )} - - {service.compilationStatus} · v{service.version} - - {service.description && ( -

- {service.description} -

+ + {/* Sag taraf */} + {service.lastSuccessfulCompilation && ( +
+ + + {new Date(service.lastSuccessfulCompilation).toLocaleDateString()} + +
)}
- {service.lastSuccessfulCompilation && ( -
- - - {new Date(service.lastSuccessfulCompilation).toLocaleDateString()} - -
- )} -
- {/* Actions */} -
- - - -
-
- ))} -
+ ))} +
+ ) : ( +
+ + + + + + + + + + + + + + {filteredServices.map((service) => ( + + + + + + + + + + ))} + +
+ {translate('::App.Platform.Name')} + + {translate('::App.Platform.DisplayName')} + + {translate('::App.Platform.Description')} + + {translate('::App.Listform.ListformField.Version')} + + {translate('::App.Platform.Modified')} + + {translate('::App.Platform.Status')} + + {translate('::App.Platform.Actions')} +
+
+
+ + {service.name} + +
+
+ {service.displayName} + + {service.description} + + v{service.version} + + {service.lastSuccessfulCompilation + ? new Date(service.lastSuccessfulCompilation).toLocaleDateString() + : ''} + {renderStatusBadge(service)} +
{renderActions(service)}
+
+
+ ) ) : (
diff --git a/ui/src/views/developerKit/VisualComponentDesigner.tsx b/ui/src/views/developerKit/VisualComponentDesigner.tsx index 403629e6..dbcd097b 100644 --- a/ui/src/views/developerKit/VisualComponentDesigner.tsx +++ b/ui/src/views/developerKit/VisualComponentDesigner.tsx @@ -1048,7 +1048,7 @@ const ObjectListEditor = ({ const { translate } = useLocalization() const items = Array.isArray(value) ? (value as Array>) : [] const inputClass = - 'w-full rounded border border-slate-300 bg-white px-2 py-1.5 text-[10px] text-slate-800 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100' + 'w-full rounded border border-slate-300 bg-white px-2 py-1.5 text-[10px] text-slate-800 outline-none focus:border-sky-500 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100' const replaceItem = (index: number, next: Record) => onChange(items.map((item, itemIndex) => (itemIndex === index ? next : item))) @@ -1066,7 +1066,7 @@ const ObjectListEditor = ({ {items.map((item, index) => (
@@ -1104,7 +1104,7 @@ const ObjectListEditor = ({ {item.src ? ( ) : null} @@ -1132,7 +1132,7 @@ const ObjectListEditor = ({
))}
-
+
{(['all', ...DESIGNER_HTTP_METHODS] as const).map((method) => (
{endpointCatalogLoading && !visibleSources.length ? ( -

+

{translate('::App.DeveloperKitComponentDesigner.LoadingSavedEndpoints')}

) : visibleSources.length ? ( @@ -3483,10 +3483,10 @@ const VisualComponentDesigner = () => { {visibleSources.map((item) => (
-
+
{item.source.name}
@@ -3514,7 +3514,7 @@ const VisualComponentDesigner = () => { {item.originType === 'component' && ( <>
) : ( -

+

{translate( hasFilter ? '::App.DeveloperKitComponentDesigner.NoMatchingEndpoint' @@ -3558,17 +3558,17 @@ const VisualComponentDesigner = () => { if (!isOptionDataComponent(selectedNode?.type) && !isTabularDataComponent(selectedNode?.type)) return null return ( -

+
{translate('::App.Listform.ListformField.DataSourceType')}
-
+
{(['static', 'endpoint'] as const).map((mode) => (