claude instructions dosyaları düzenlendi

This commit is contained in:
Sedat ÖZTÜRK 2026-08-26 21:44:53 +03:00
parent 422e32489f
commit 9981d5b19a
12 changed files with 3104 additions and 288 deletions

View file

@ -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:

View file

@ -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.

File diff suppressed because it is too large Load diff

334
README.md
View file

@ -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` | Anadetay 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
ı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ıı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.<ad>.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 <Ad>
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.<Modul> -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.
- Rolyetki eşleşmeleri veritabanında müşteri ortamına göre değişir; buradaki yetki kodları
tanımın kendisidir, atama değil.

View file

@ -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ıı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.

View file

@ -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

View file

@ -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<void>
clients: {
claim: () => Promise<void>
@ -133,6 +133,16 @@ async function fetchWarmupList(): Promise<WarmupEntry[]> {
let warmupPromise: Promise<WarmupEntry[]> | 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ç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)

View file

@ -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 && (
<Button
type="button"
variant="solid"
color="blue-600"
variant="plain"
shape="none"
title={translate('::App.Platform.Edit')}
icon={<FaEdit className="h-4 w-4" />}
className={`${ACTION_BUTTON_CLASS} ${ACTION_BUTTON_HOVER.blue}`}
onClick={() =>
navigate(ROUTES_ENUM.protected.saas.listFormManagement.wizard, {
state: { editFileName: f.fileName },
})
}
>
<FaEdit className="w-4 h-4" />
</Button>
/>
)}
{/* Menünün açtığı adres yeni sekmede açılır; adres yoksa buton anlamsızdır. */}
{f.menuUrl && (
<Button
type="button"
variant="solid"
color="gray-600"
variant="plain"
shape="none"
title={translate('::App.Platform.OpenUrl')}
icon={<FaExternalLinkAlt className="h-4 w-4" />}
className={`${ACTION_BUTTON_CLASS} ${ACTION_BUTTON_HOVER.slate}`}
onClick={() => window.open(f.menuUrl, '_blank', 'noopener,noreferrer')}
>
<FaExternalLinkAlt className="w-4 h-4" />
</Button>
/>
)}
{canExport && (
<Button
type="button"
variant="solid"
color="green-600"
variant="plain"
shape="none"
title={translate('::App.ListForm.WizardFileExport')}
loading={exportingFile === f.fileName}
icon={<FaFileExport className="h-4 w-4" />}
className={`${ACTION_BUTTON_CLASS} ${ACTION_BUTTON_HOVER.green}`}
onClick={() => handleExport(f)}
>
<FaFileExport className="w-4 h-4" />
</Button>
/>
)}
{canDelete && (
<Button
type="button"
variant="solid"
color="red-600"
variant="plain"
shape="none"
title={translate('::App.Platform.Delete')}
loading={deletingFile === f.fileName}
icon={<FaTrash className="h-4 w-4" />}
className={`${ACTION_BUTTON_CLASS} ${ACTION_BUTTON_HOVER.red}`}
onClick={() =>
setConfirm({ fileName: f.fileName, wizardName: f.wizardName || f.fileName })
}
>
<FaTrash className="w-4 h-4" />
</Button>
/>
)}
</div>
)

View file

@ -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 && (
<Button
type="button"
variant="solid"
color="blue-600"
variant="plain"
shape="none"
title={translate('::App.Platform.Edit')}
icon={<FaRegEdit className="h-4 w-4" />}
className={`${ACTION_BUTTON_CLASS} ${ACTION_BUTTON_HOVER.blue}`}
onClick={() =>
window.open(
ROUTES_ENUM.protected.saas.developerKit.componentsEdit.replace(':id', component.id),
'_blank',
)
}
>
<FaRegEdit className="w-4 h-4" />
</Button>
/>
)}
<Button
type="button"
variant="solid"
color="gray-600"
variant="plain"
shape="none"
disabled={!component.isActive || !component.routePath?.trim()}
title={translate('::App.Platform.OpenUrl')}
icon={<FaExternalLinkAlt className="h-4 w-4" />}
className={`${ACTION_BUTTON_CLASS} ${ACTION_BUTTON_HOVER.slate}`}
onClick={() => {
const routePath = component.routePath.startsWith('/')
? component.routePath
: `/${component.routePath}`
window.open(routePath, '_blank')
}}
>
<FaExternalLinkAlt className="w-4 h-4" />
</Button>
/>
<Button
type="button"
variant="solid"
color="green-600"
variant="plain"
shape="none"
title={translate('::App.Platform.View')}
icon={<FaCog className="h-4 w-4" />}
className={`${ACTION_BUTTON_CLASS} ${ACTION_BUTTON_HOVER.green}`}
onClick={() => setEditorComponentId(component.id)}
>
<FaCog className="w-4 h-4" />
</Button>
/>
{canDelete && (
<Button
type="button"
variant="solid"
color="red-600"
variant="plain"
shape="none"
title={translate('::App.Platform.Delete')}
icon={<FaTrashAlt className="h-4 w-4" />}
className={`${ACTION_BUTTON_CLASS} ${ACTION_BUTTON_HOVER.red}`}
onClick={() => handleDelete(component.id)}
>
<FaTrashAlt className="w-4 h-4" />
</Button>
/>
)}
</div>
)

View file

@ -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) => (
<span
className={`inline-block px-2 py-0.5 text-xs rounded-full font-medium whitespace-nowrap ${statusBadge(
service.compilationStatus,
)}`}
>
{service.compilationStatus} · v{service.version}
</span>
)
const renderActions = (service: DynamicServiceDto) => (
<div className="flex items-center gap-1">
<Button
type="button"
variant="plain"
shape="none"
title={translate('::App.Platform.Edit')}
icon={<FaRegEdit className="h-4 w-4" />}
className={`${ACTION_BUTTON_CLASS} ${ACTION_BUTTON_HOVER.blue}`}
onClick={() =>
navigate(
ROUTES_ENUM.protected.saas.developerKit.dynamicServicesEdit.replace(':id', service.id),
)
}
/>
<Button
type="button"
variant="plain"
shape="none"
title={translate('::App.Platform.Delete')}
icon={<FaTrashAlt className="h-4 w-4" />}
className={`${ACTION_BUTTON_CLASS} ${ACTION_BUTTON_HOVER.red}`}
onClick={() => deleteService(service.id)}
/>
</div>
)
return (
<div className="space-y-4">
<PageTitle title={translate('::' + 'App.DeveloperKit.DynamicServices')} />
@ -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)}
/>
</div>
<div className="flex items-center gap-2 w-full lg:w-auto">
@ -161,11 +215,11 @@ const DynamicServiceManager: React.FC = () => {
<div className="w-full sm:w-auto">
<Button
type="button"
onClick={openSwagger}
variant="solid"
color="green-600"
icon={<FaExternalLinkAlt className="h-3 w-3" />}
className="w-full !h-auto !items-center !justify-center gap-2 whitespace-nowrap !rounded-lg px-4 py-2 text-white transition-colors hover:!bg-green-700 dark:!bg-green-700 dark:hover:!bg-green-800 sm:w-auto"
onClick={openSwagger}
>
Swagger
</Button>
@ -179,6 +233,22 @@ const DynamicServiceManager: React.FC = () => {
{translate('::App.DeveloperKitDynamicServices.NewService')}
</Link>
</div>
<div className="flex items-center gap-1">
<Button
type="button"
icon={<FaTh />}
variant={viewMode === 'card' ? 'solid' : 'default'}
title={translate('::App.Platform.CardView')}
onClick={() => setViewMode('card')}
/>
<Button
type="button"
icon={<FaList />}
variant={viewMode === 'grid' ? 'solid' : 'default'}
title={translate('::App.Platform.ListView')}
onClick={() => setViewMode('grid')}
/>
</div>
</div>
{/* List */}
@ -187,79 +257,138 @@ const DynamicServiceManager: React.FC = () => {
<FaSpinner className="w-8 h-8 animate-spin text-slate-400" />
</div>
) : filteredServices.length > 0 ? (
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-6">
{filteredServices.map((service) => (
<div
key={service.id}
className="bg-white dark:bg-gray-900 rounded-lg border border-slate-200 dark:border-gray-700 shadow-sm hover:shadow-md transition-shadow"
>
<div className="p-6">
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<h3 className="text-base font-semibold text-slate-900 dark:text-gray-100">
{service.name}
</h3>
<div
className={`w-2 h-2 rounded-full ${
service.compilationStatus === 'Success'
? 'bg-emerald-500'
: 'bg-slate-300 dark:bg-gray-700'
}`}
/>
viewMode === 'card' ? (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{filteredServices.map((service) => (
<div
key={service.id}
className="bg-white dark:bg-gray-900 rounded-lg border border-slate-200 dark:border-gray-700 shadow-sm hover:shadow-md transition-shadow"
>
<div className="p-4">
<div className="flex items-start justify-between">
{/* Sol taraf */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<h3
className="text-base font-semibold text-slate-900 dark:text-gray-100 truncate"
title={service.name}
>
{service.name}
</h3>
<div
className={`w-2 h-2 shrink-0 rounded-full ${
service.isActive ? 'bg-emerald-500' : 'bg-slate-300 dark:bg-gray-700'
}`}
/>
</div>
{service.displayName && (
<p
className="text-slate-600 dark:text-gray-300 text-sm mb-2 truncate"
title={service.displayName}
>
{service.displayName}
</p>
)}
<div className="mb-2">{renderStatusBadge(service)}</div>
{service.description && (
<p className="text-slate-600 dark:text-gray-300 text-sm mb-2 line-clamp-2">
{service.description}
</p>
)}
</div>
{service.displayName && (
<p className="text-slate-500 dark:text-gray-400 text-sm mb-1">
{service.displayName}
</p>
)}
<span
className={`inline-block px-2 py-0.5 text-xs rounded-full font-medium mb-3 ${statusBadge(service.compilationStatus)} dark:bg-opacity-80`}
>
{service.compilationStatus} · v{service.version}
</span>
{service.description && (
<p className="text-slate-500 dark:text-gray-400 text-sm">
{service.description}
</p>
{/* Sag taraf */}
{service.lastSuccessfulCompilation && (
<div className="flex items-center gap-1 text-xs text-slate-500 dark:text-gray-400 ml-2 whitespace-nowrap">
<FaCalendarAlt className="w-3 h-3" />
<span>
{new Date(service.lastSuccessfulCompilation).toLocaleDateString()}
</span>
</div>
)}
</div>
{service.lastSuccessfulCompilation && (
<div className="flex items-center gap-1 text-xs text-slate-400 dark:text-gray-400 sm:ml-4 whitespace-nowrap">
<FaCalendarAlt className="w-3 h-3" />
<span>
{new Date(service.lastSuccessfulCompilation).toLocaleDateString()}
</span>
</div>
)}
</div>
{/* Actions */}
<div className="flex items-center justify-end pt-3 border-t border-slate-100 dark:border-gray-700 gap-1 mt-4">
<Link
to={ROUTES_ENUM.protected.saas.developerKit.dynamicServicesEdit.replace(
':id',
service.id,
)}
className="p-2 text-slate-500 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 hover:bg-blue-50 dark:hover:bg-gray-800 rounded transition-colors"
title={translate('::App.Platform.Edit')}
>
<FaRegEdit className="w-4 h-4" />
</Link>
<Button
type="button"
onClick={() => deleteService(service.id)}
variant="plain"
shape="none"
icon={<FaTrashAlt className="h-4 w-4" />}
className="!h-8 !w-8 !rounded !p-0 text-slate-500 transition-colors hover:!bg-red-50 hover:text-red-600 dark:text-gray-400 dark:hover:!bg-gray-800 dark:hover:text-red-400"
title={translate('::App.Platform.Delete')}
/>
{/* Actions */}
<div className="flex items-center justify-end gap-1 pt-2 border-t border-slate-100 dark:border-gray-700">
{renderActions(service)}
</div>
</div>
</div>
</div>
))}
</div>
))}
</div>
) : (
<div className="bg-white dark:bg-gray-900 rounded-lg border border-slate-200 dark:border-gray-700 shadow-sm overflow-x-auto">
<table className="w-full text-sm">
<thead className="bg-slate-50 dark:bg-gray-800 text-slate-600 dark:text-gray-300">
<tr>
<th className="text-left font-medium px-4 py-2">
{translate('::App.Platform.Name')}
</th>
<th className="text-left font-medium px-4 py-2">
{translate('::App.Platform.DisplayName')}
</th>
<th className="text-left font-medium px-4 py-2">
{translate('::App.Platform.Description')}
</th>
<th className="text-left font-medium px-4 py-2 whitespace-nowrap">
{translate('::App.Listform.ListformField.Version')}
</th>
<th className="text-left font-medium px-4 py-2 whitespace-nowrap">
{translate('::App.Platform.Modified')}
</th>
<th className="text-left font-medium px-4 py-2">
{translate('::App.Platform.Status')}
</th>
<th className="text-right font-medium px-4 py-2">
{translate('::App.Platform.Actions')}
</th>
</tr>
</thead>
<tbody>
{filteredServices.map((service) => (
<tr
key={service.id}
className="border-t border-slate-100 dark:border-gray-700 hover:bg-slate-50 dark:hover:bg-gray-800"
>
<td className="px-4 py-2">
<div className="flex items-center gap-2">
<div
className={`w-2 h-2 shrink-0 rounded-full ${
service.isActive ? 'bg-emerald-500' : 'bg-slate-300 dark:bg-gray-700'
}`}
/>
<span className="font-medium text-slate-900 dark:text-gray-100">
{service.name}
</span>
</div>
</td>
<td className="px-4 py-2 text-slate-600 dark:text-gray-300">
{service.displayName}
</td>
<td className="px-4 py-2 text-slate-600 dark:text-gray-300">
{service.description}
</td>
<td className="px-4 py-2 text-slate-600 dark:text-gray-300 whitespace-nowrap">
v{service.version}
</td>
<td className="px-4 py-2 text-slate-500 dark:text-gray-400 whitespace-nowrap">
{service.lastSuccessfulCompilation
? new Date(service.lastSuccessfulCompilation).toLocaleDateString()
: ''}
</td>
<td className="px-4 py-2">{renderStatusBadge(service)}</td>
<td className="px-4 py-2">
<div className="flex justify-end">{renderActions(service)}</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)
) : (
<div className="text-center py-12">
<div className="bg-slate-100 dark:bg-gray-800 rounded-full w-16 h-16 flex items-center justify-center mx-auto mb-4">

View file

@ -1048,7 +1048,7 @@ const ObjectListEditor = ({
const { translate } = useLocalization()
const items = Array.isArray(value) ? (value as Array<Record<string, unknown>>) : []
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<string, unknown>) =>
onChange(items.map((item, itemIndex) => (itemIndex === index ? next : item)))
@ -1066,7 +1066,7 @@ const ObjectListEditor = ({
{items.map((item, index) => (
<div
key={index}
className="space-y-1.5 rounded-md border border-slate-200 p-2 dark:border-slate-700"
className="space-y-1.5 rounded-md border border-slate-200 p-2 dark:border-gray-700"
>
<div className="flex items-center justify-between">
<span className="text-[10px] font-semibold text-slate-500">
@ -1104,7 +1104,7 @@ const ObjectListEditor = ({
{item.src ? (
<img
alt=""
className="h-16 w-full rounded border border-slate-200 object-cover dark:border-slate-700"
className="h-16 w-full rounded border border-slate-200 object-cover dark:border-gray-700"
src={String(item.thumbnail || item.src)}
/>
) : null}
@ -1132,7 +1132,7 @@ const ObjectListEditor = ({
</div>
))}
<button
className="flex w-full items-center justify-center gap-1 rounded border border-dashed border-slate-300 px-2 py-1.5 text-[10px] font-semibold text-sky-600 hover:border-sky-400 dark:border-slate-700"
className="flex w-full items-center justify-center gap-1 rounded border border-dashed border-slate-300 px-2 py-1.5 text-[10px] font-semibold text-sky-600 hover:border-sky-400 dark:border-gray-700"
type="button"
onClick={() => onChange([...items, { src: '' }])}
>
@ -1160,12 +1160,12 @@ const PropertyEditor = ({
const { translate } = useLocalization()
const [styleModalOpen, setStyleModalOpen] = useState(false)
const inputClass =
'w-full rounded-md border border-slate-300 bg-white px-2.5 py-2 text-xs text-slate-800 outline-none transition focus:border-sky-500 focus:ring-2 focus:ring-sky-100 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100'
'w-full rounded-md border border-slate-300 bg-white px-2.5 py-2 text-xs text-slate-800 outline-none transition focus:border-sky-500 focus:ring-2 focus:ring-sky-100 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100'
if (type === 'boolean') {
return (
<label className="flex cursor-pointer items-center justify-between rounded-md border border-slate-200 px-2.5 py-2 dark:border-slate-700">
<span className="text-xs text-slate-600 dark:text-slate-300">
<label className="flex cursor-pointer items-center justify-between rounded-md border border-slate-200 px-2.5 py-2 dark:border-gray-700">
<span className="text-xs text-slate-600 dark:text-gray-300">
{value ? 'Enabled' : 'Disabled'}
</span>
<Input
@ -3429,20 +3429,20 @@ const VisualComponentDesigner = () => {
<FaSearch className="pointer-events-none absolute left-2.5 top-1/2 -translate-y-1/2 text-[10px] text-slate-400" />
<Input
unstyle
className="w-full rounded-md border border-slate-300 bg-white py-2 pl-7 pr-2.5 text-xs dark:border-slate-700 dark:bg-slate-900"
className="w-full rounded-md border border-slate-300 bg-white py-2 pl-7 pr-2.5 text-xs dark:border-gray-700 dark:bg-gray-900"
placeholder={translate('::App.DeveloperKitComponentDesigner.SearchEndpoint')}
type="search"
value={endpointSearch}
onChange={(event) => setEndpointSearch(event.target.value)}
/>
</div>
<div className="flex items-center gap-1 rounded-md bg-slate-100 p-1 dark:bg-slate-950">
<div className="flex items-center gap-1 rounded-md bg-slate-100 p-1 dark:bg-gray-950">
{(['all', ...DESIGNER_HTTP_METHODS] as const).map((method) => (
<button
key={method}
className={`rounded px-2 py-1 text-[10px] font-semibold uppercase ${
endpointMethodFilter === method
? 'bg-white text-sky-700 shadow-sm dark:bg-slate-800 dark:text-sky-300'
? 'bg-white text-sky-700 shadow-sm dark:bg-gray-800 dark:text-sky-300'
: 'text-slate-500'
}`}
type="button"
@ -3464,12 +3464,12 @@ const VisualComponentDesigner = () => {
/>
{translate('::App.DeveloperKitComponentDesigner.AttachedOnly')}
</label>
<span className="shrink-0 rounded bg-slate-100 px-2 py-1 text-[10px] font-semibold text-slate-500 dark:bg-slate-950">
<span className="shrink-0 rounded bg-slate-100 px-2 py-1 text-[10px] font-semibold text-slate-500 dark:bg-gray-950">
{visibleSources.length} / {dataSourceCatalog.length}
</span>
</div>
{endpointCatalogLoading && !visibleSources.length ? (
<p className="rounded-lg border border-dashed border-slate-300 p-3 text-center text-xs text-slate-500 dark:border-slate-700">
<p className="rounded-lg border border-dashed border-slate-300 p-3 text-center text-xs text-slate-500 dark:border-gray-700">
{translate('::App.DeveloperKitComponentDesigner.LoadingSavedEndpoints')}
</p>
) : visibleSources.length ? (
@ -3483,10 +3483,10 @@ const VisualComponentDesigner = () => {
{visibleSources.map((item) => (
<div
key={item.key}
className="flex items-center gap-2 rounded-lg border border-slate-200 bg-white p-2.5 dark:border-slate-800 dark:bg-slate-900"
className="flex items-center gap-2 rounded-lg border border-slate-200 bg-white p-2.5 dark:border-gray-700 dark:bg-gray-900"
>
<div className="min-w-0 flex-1">
<div className="truncate text-xs font-semibold text-slate-700 dark:text-slate-200">
<div className="truncate text-xs font-semibold text-slate-700 dark:text-gray-200">
{item.source.name}
</div>
<div className="truncate font-mono text-[9px] text-sky-600">
@ -3514,7 +3514,7 @@ const VisualComponentDesigner = () => {
{item.originType === 'component' && (
<>
<button
className="rounded-md border border-slate-200 p-1.5 text-slate-500 hover:border-amber-400 hover:text-amber-600 dark:border-slate-700"
className="rounded-md border border-slate-200 p-1.5 text-slate-500 hover:border-amber-400 hover:text-amber-600 dark:border-gray-700"
title={translate('::App.DeveloperKitComponentDesigner.EditEndpoint')}
type="button"
onClick={() => openCatalogSourceEditor(item)}
@ -3522,7 +3522,7 @@ const VisualComponentDesigner = () => {
<FaEdit />
</button>
<button
className="rounded-md border border-slate-200 p-1.5 text-slate-500 hover:border-red-400 hover:text-red-600 dark:border-slate-700"
className="rounded-md border border-slate-200 p-1.5 text-slate-500 hover:border-red-400 hover:text-red-600 dark:border-gray-700"
title={translate('::App.DeveloperKitComponentDesigner.DeleteEndpoint')}
type="button"
onClick={() => void deleteCatalogSource(item)}
@ -3536,7 +3536,7 @@ const VisualComponentDesigner = () => {
))}
</div>
) : (
<p className="rounded-lg border border-dashed border-slate-300 p-3 text-center text-xs leading-5 text-slate-500 dark:border-slate-700">
<p className="rounded-lg border border-dashed border-slate-300 p-3 text-center text-xs leading-5 text-slate-500 dark:border-gray-700">
{translate(
hasFilter
? '::App.DeveloperKitComponentDesigner.NoMatchingEndpoint'
@ -3558,17 +3558,17 @@ const VisualComponentDesigner = () => {
if (!isOptionDataComponent(selectedNode?.type) && !isTabularDataComponent(selectedNode?.type))
return null
return (
<div className="rounded-lg border border-slate-200 p-3 dark:border-slate-800">
<div className="rounded-lg border border-slate-200 p-3 dark:border-gray-700">
<div className="mb-2 text-[10px] font-semibold uppercase tracking-wider text-slate-400">
{translate('::App.Listform.ListformField.DataSourceType')}
</div>
<div className="grid grid-cols-2 gap-1 rounded-md bg-slate-100 p-1 dark:bg-slate-950">
<div className="grid grid-cols-2 gap-1 rounded-md bg-slate-100 p-1 dark:bg-gray-950">
{(['static', 'endpoint'] as const).map((mode) => (
<button
key={mode}
className={`rounded px-2 py-1.5 text-[10px] font-semibold ${
selectDataMode === mode
? 'bg-white text-sky-700 shadow-sm dark:bg-slate-800 dark:text-sky-300'
? 'bg-white text-sky-700 shadow-sm dark:bg-gray-800 dark:text-sky-300'
: 'text-slate-500'
}`}
type="button"
@ -3589,7 +3589,7 @@ const VisualComponentDesigner = () => {
const renderSelectDataConfiguration = () => {
if (!isOptionDataComponent(selectedNode?.type)) return null
return (
<div className="rounded-lg border border-slate-200 p-3 dark:border-slate-800">
<div className="rounded-lg border border-slate-200 p-3 dark:border-gray-700">
{selectDataMode === 'static' ? (
<div className="space-y-2">
<div className="mb-2 text-[10px] font-semibold uppercase tracking-wider text-slate-400">
@ -3603,7 +3603,7 @@ const VisualComponentDesigner = () => {
aria-label={translate('::App.DeveloperKitComponentDesigner.OptionLabelAria', {
index: index + 1,
})}
className="min-w-0 rounded border border-slate-300 bg-white px-2 py-1.5 text-[10px] dark:border-slate-700 dark:bg-slate-900"
className="min-w-0 rounded border border-slate-300 bg-white px-2 py-1.5 text-[10px] dark:border-gray-700 dark:bg-gray-900"
placeholder={translate('::App.DeveloperKitComponentDesigner.LabelHeader')}
value={option.label}
onChange={(event) =>
@ -3621,7 +3621,7 @@ const VisualComponentDesigner = () => {
aria-label={translate('::App.DeveloperKitComponentDesigner.OptionValueAria', {
index: index + 1,
})}
className="min-w-0 rounded border border-slate-300 bg-white px-2 py-1.5 text-[10px] dark:border-slate-700 dark:bg-slate-900"
className="min-w-0 rounded border border-slate-300 bg-white px-2 py-1.5 text-[10px] dark:border-gray-700 dark:bg-gray-900"
placeholder={translate('::App.Listform.ListformField.Value')}
value={String(option.value ?? '')}
onChange={(event) =>
@ -3664,7 +3664,7 @@ const VisualComponentDesigner = () => {
{selectedNode?.type === 'Tabs' && (
<Input
unstyle
className="w-full min-w-0 rounded border border-slate-300 bg-white px-2 py-1.5 text-[10px] dark:border-slate-700 dark:bg-slate-900"
className="w-full min-w-0 rounded border border-slate-300 bg-white px-2 py-1.5 text-[10px] dark:border-gray-700 dark:bg-gray-900"
placeholder={translate(
'::App.DeveloperKitComponentDesigner.TabContentPlaceholder',
)}
@ -3683,7 +3683,7 @@ const VisualComponentDesigner = () => {
</div>
))}
<button
className="flex w-full items-center justify-center gap-1 rounded border border-dashed border-slate-300 px-2 py-1.5 text-[10px] font-semibold text-sky-600 hover:border-sky-400 dark:border-slate-700"
className="flex w-full items-center justify-center gap-1 rounded border border-dashed border-slate-300 px-2 py-1.5 text-[10px] font-semibold text-sky-600 hover:border-sky-400 dark:border-gray-700"
type="button"
onClick={() =>
updateStaticSelectOptions([
@ -3856,7 +3856,7 @@ const VisualComponentDesigner = () => {
</>
)}
{activeDataSample === undefined && (
<p className="rounded bg-slate-50 p-2 text-[10px] leading-4 text-slate-500 dark:bg-slate-950">
<p className="rounded bg-slate-50 p-2 text-[10px] leading-4 text-slate-500 dark:bg-gray-950">
{translate('::App.DeveloperKitComponentDesigner.SelectEndpointFirst')}
</p>
)}
@ -3873,7 +3873,7 @@ const VisualComponentDesigner = () => {
const draft = staticCollectionDrafts[selectedId] ?? JSON.stringify(currentItems, null, 2)
const error = staticCollectionErrors[selectedId]
return (
<div className="rounded-lg border border-slate-200 p-3 dark:border-slate-800">
<div className="rounded-lg border border-slate-200 p-3 dark:border-gray-700">
<div className="mb-2 text-[10px] font-semibold uppercase tracking-wider text-slate-400">
{translate('::App.DeveloperKitComponentDesigner.StaticRowsJson')}
</div>
@ -3881,7 +3881,7 @@ const VisualComponentDesigner = () => {
{translate('::App.DeveloperKitComponentDesigner.StaticRowsHint')}
</p>
<textarea
className="min-h-40 w-full rounded-md border border-slate-300 bg-white p-2 font-mono text-[10px] leading-4 dark:border-slate-700 dark:bg-slate-900"
className="min-h-40 w-full rounded-md border border-slate-300 bg-white p-2 font-mono text-[10px] leading-4 dark:border-gray-700 dark:bg-gray-900"
spellCheck={false}
value={draft}
onChange={(event) => updateStaticCollection(event.target.value)}
@ -3915,7 +3915,7 @@ const VisualComponentDesigner = () => {
: Object.keys(gridColumnLookups)
if (!columns.length) return null
return (
<div className="rounded-lg border border-slate-200 p-3 dark:border-slate-800">
<div className="rounded-lg border border-slate-200 p-3 dark:border-gray-700">
<div className="mb-1 text-[10px] font-semibold uppercase tracking-wider text-slate-400">
{translate('::App.DeveloperKitComponentDesigner.ColumnDisplay')}
</div>
@ -3937,7 +3937,7 @@ const VisualComponentDesigner = () => {
return (
<div
key={column}
className="rounded border border-slate-200 bg-slate-50 p-2 dark:border-slate-800 dark:bg-slate-950"
className="rounded border border-slate-200 bg-slate-50 p-2 dark:border-gray-700 dark:bg-gray-950"
>
<code className="block truncate text-[10px] text-sky-700 dark:text-sky-300">
{column}
@ -3947,7 +3947,7 @@ const VisualComponentDesigner = () => {
renamed. Left empty it keeps the field name. */}
<Input
unstyle
className="mt-1.5 w-full rounded-md border border-slate-300 bg-white px-2 py-1.5 text-[10px] dark:border-slate-700 dark:bg-slate-900"
className="mt-1.5 w-full rounded-md border border-slate-300 bg-white px-2 py-1.5 text-[10px] dark:border-gray-700 dark:bg-gray-900"
placeholder={translate(
'::App.DeveloperKitComponentDesigner.ColumnCaptionPlaceholder',
{ column },
@ -4062,7 +4062,7 @@ const VisualComponentDesigner = () => {
const renderTabularColumnConfiguration = () => {
if (!isTabularDataComponent(selectedNode?.type)) return null
return (
<div className="rounded-lg border border-slate-200 p-3 dark:border-slate-800">
<div className="rounded-lg border border-slate-200 p-3 dark:border-gray-700">
<div className="mb-2 flex items-center justify-between">
<span className="text-[10px] font-semibold uppercase tracking-wider text-slate-400">
{translate('::App.DeveloperKitComponentDesigner.VisibleColumns')}
@ -4082,7 +4082,7 @@ const VisualComponentDesigner = () => {
{gridColumnFields.map((field) => (
<label
key={field.path}
className="flex cursor-pointer items-center gap-2 rounded bg-slate-50 px-2 py-1.5 dark:bg-slate-950"
className="flex cursor-pointer items-center gap-2 rounded bg-slate-50 px-2 py-1.5 dark:bg-gray-950"
>
<Input
unstyle
@ -4099,7 +4099,7 @@ const VisualComponentDesigner = () => {
))}
</div>
) : (
<p className="rounded bg-slate-50 p-2 text-[10px] leading-4 text-slate-500 dark:bg-slate-950">
<p className="rounded bg-slate-50 p-2 text-[10px] leading-4 text-slate-500 dark:bg-gray-950">
{translate('::App.DeveloperKitComponentDesigner.AddObjectForColumns')}
</p>
)}
@ -4209,7 +4209,7 @@ const VisualComponentDesigner = () => {
}))
return (
<div className="rounded-lg border border-slate-200 p-3 dark:border-slate-800">
<div className="rounded-lg border border-slate-200 p-3 dark:border-gray-700">
<div className="mb-1 flex items-center justify-between">
<span className="text-[10px] font-semibold uppercase tracking-wider text-slate-400">
{translate('::App.DeveloperKitComponentDesigner.Filters')}
@ -4236,7 +4236,7 @@ const VisualComponentDesigner = () => {
))}
</datalist>
{!filters.length && (
<p className="rounded bg-slate-50 p-2 text-[10px] leading-4 text-slate-500 dark:bg-slate-950">
<p className="rounded bg-slate-50 p-2 text-[10px] leading-4 text-slate-500 dark:bg-gray-950">
{translate('::App.DeveloperKitComponentDesigner.NoFilter')}
</p>
)}
@ -4257,7 +4257,7 @@ const VisualComponentDesigner = () => {
return (
<div
key={filter.id}
className="space-y-1.5 rounded-md border border-slate-200 bg-slate-50 p-2 dark:border-slate-800 dark:bg-slate-950"
className="space-y-1.5 rounded-md border border-slate-200 bg-slate-50 p-2 dark:border-gray-700 dark:bg-gray-950"
>
<div className="flex items-center gap-1.5">
{strictColumns ? (
@ -4278,7 +4278,7 @@ const VisualComponentDesigner = () => {
) : (
<Input
unstyle
className="min-w-0 flex-1 rounded border border-slate-300 bg-white px-2 py-1 text-[10px] dark:border-slate-700 dark:bg-slate-900"
className="min-w-0 flex-1 rounded border border-slate-300 bg-white px-2 py-1 text-[10px] dark:border-gray-700 dark:bg-gray-900"
list={listId}
placeholder={translate('::App.DeveloperKitComponentDesigner.FilterColumn')}
value={filter.field || ''}
@ -4334,7 +4334,7 @@ const VisualComponentDesigner = () => {
{filter.source !== 'record' && (
<Input
unstyle
className="min-w-0 flex-1 rounded border border-slate-300 bg-white px-2 py-1 text-[10px] dark:border-slate-700 dark:bg-slate-900"
className="min-w-0 flex-1 rounded border border-slate-300 bg-white px-2 py-1 text-[10px] dark:border-gray-700 dark:bg-gray-900"
placeholder={translate(
filter.source === 'static'
? '::App.DeveloperKitComponentDesigner.FilterValue'
@ -4373,7 +4373,7 @@ const VisualComponentDesigner = () => {
/>
<Input
unstyle
className="min-w-0 flex-1 rounded border border-slate-300 bg-white px-2 py-1 text-[10px] dark:border-slate-700 dark:bg-slate-900"
className="min-w-0 flex-1 rounded border border-slate-300 bg-white px-2 py-1 text-[10px] dark:border-gray-700 dark:bg-gray-900"
list={`${listId}-${filter.id}-master`}
placeholder={translate(
'::App.DeveloperKitComponentDesigner.FilterMasterColumn',
@ -4400,7 +4400,7 @@ const VisualComponentDesigner = () => {
{!valueless && filter.source !== 'static' && (
<Input
unstyle
className="w-full rounded border border-dashed border-slate-300 bg-white px-2 py-1 text-[10px] dark:border-slate-700 dark:bg-slate-900"
className="w-full rounded border border-dashed border-slate-300 bg-white px-2 py-1 text-[10px] dark:border-gray-700 dark:bg-gray-900"
placeholder={translate(
'::App.DeveloperKitComponentDesigner.FilterPreviewValue',
)}
@ -4478,7 +4478,7 @@ const VisualComponentDesigner = () => {
// close the permission menu the instant it opens.
<div
key={slot.property}
className="block rounded-lg border border-slate-200 p-3 dark:border-slate-800"
className="block rounded-lg border border-slate-200 p-3 dark:border-gray-700"
>
<span className="mb-1 flex items-center justify-between text-[10px] font-semibold uppercase tracking-wider text-slate-400">
<span>{slot.label}</span>
@ -4547,7 +4547,7 @@ const VisualComponentDesigner = () => {
</span>
<Input
size="xs"
className="w-full rounded-md border border-slate-300 bg-white px-2 py-2 text-[10px] dark:border-slate-700 dark:bg-slate-900"
className="w-full rounded-md border border-slate-300 bg-white px-2 py-2 text-[10px] dark:border-gray-700 dark:bg-gray-900"
placeholder="id"
value={String(selectedNode.props.keyFieldName ?? '')}
onChange={(event) => updateSelectedProp('keyFieldName', event.target.value)}
@ -4576,7 +4576,7 @@ const VisualComponentDesigner = () => {
/>
</label>
</div>
<div className="rounded-lg border border-slate-200 p-3 dark:border-slate-800">
<div className="rounded-lg border border-slate-200 p-3 dark:border-gray-700">
<div className="mb-1 text-[10px] font-semibold uppercase tracking-wider text-slate-400">
Select key parametresi
</div>
@ -4594,7 +4594,7 @@ const VisualComponentDesigner = () => {
/>
<Input
unstyle
className="w-full rounded-md border border-slate-300 bg-white px-2 py-2 text-[10px] dark:border-slate-700 dark:bg-slate-900"
className="w-full rounded-md border border-slate-300 bg-white px-2 py-2 text-[10px] dark:border-gray-700 dark:bg-gray-900"
placeholder={translate('::App.DeveloperKitComponentDesigner.KeyParamPlaceholder', {
field: getFormKeyField(selectedNode),
})}
@ -4604,7 +4604,7 @@ const VisualComponentDesigner = () => {
</div>
{renderDataSourceFilters(selectSource)}
{selectNeedsPreviewKey && (
<div className="rounded-lg border border-slate-200 p-3 dark:border-slate-800">
<div className="rounded-lg border border-slate-200 p-3 dark:border-gray-700">
<div className="mb-1 text-[10px] font-semibold uppercase tracking-wider text-slate-400">
{translate('::App.DeveloperKitComponentDesigner.PreviewKeyValue')}
</div>
@ -4613,7 +4613,7 @@ const VisualComponentDesigner = () => {
</p>
<Input
unstyle
className="w-full rounded-md border border-slate-300 bg-white px-2 py-2 text-[10px] dark:border-slate-700 dark:bg-slate-900"
className="w-full rounded-md border border-slate-300 bg-white px-2 py-2 text-[10px] dark:border-gray-700 dark:bg-gray-900"
placeholder={translate(
'::App.DeveloperKitComponentDesigner.PreviewKeyValuePlaceholder',
)}
@ -4622,7 +4622,7 @@ const VisualComponentDesigner = () => {
/>
</div>
)}
<div className="space-y-1.5 rounded-lg border border-slate-200 p-3 dark:border-slate-800">
<div className="space-y-1.5 rounded-lg border border-slate-200 p-3 dark:border-gray-700">
{(
[
['autoLoad', 'App.DeveloperKitComponentDesigner.AutoLoad', true],
@ -4645,7 +4645,7 @@ const VisualComponentDesigner = () => {
</label>
))}
</div>
<div className="rounded-lg border border-slate-200 p-3 dark:border-slate-800">
<div className="rounded-lg border border-slate-200 p-3 dark:border-gray-700">
<div className="mb-2 text-[10px] font-semibold uppercase tracking-wider text-slate-400">
{translate('::App.DeveloperKitComponentDesigner.RecordColumns')}
</div>
@ -4654,7 +4654,7 @@ const VisualComponentDesigner = () => {
{recordFields.map((field) => (
<div
key={field.path}
className="flex items-center justify-between gap-2 rounded bg-slate-50 px-2 py-1.5 dark:bg-slate-950"
className="flex items-center justify-between gap-2 rounded bg-slate-50 px-2 py-1.5 dark:bg-gray-950"
title={field.preview}
>
<code className="min-w-0 flex-1 truncate text-[10px] text-sky-700 dark:text-sky-300">
@ -4762,7 +4762,7 @@ const VisualComponentDesigner = () => {
: 'text'
return (
<div className="space-y-3 rounded-lg border border-slate-200 p-3 dark:border-slate-800">
<div className="space-y-3 rounded-lg border border-slate-200 p-3 dark:border-gray-700">
<div>
<div className="mb-1 text-[10px] font-semibold uppercase tracking-wider text-slate-400">
{translate('::App.DeveloperKitComponentDesigner.RecordField')} · {sqlRecordProperty}
@ -4772,7 +4772,7 @@ const VisualComponentDesigner = () => {
</p>
<Input
unstyle
className="w-full rounded-md border border-slate-300 bg-white px-2 py-2 text-[10px] dark:border-slate-700 dark:bg-slate-900"
className="w-full rounded-md border border-slate-300 bg-white px-2 py-2 text-[10px] dark:border-gray-700 dark:bg-gray-900"
list={columnListId}
placeholder={translate('::App.DeveloperKitComponentDesigner.DoNotBindToRecord')}
value={column}
@ -4817,7 +4817,7 @@ const VisualComponentDesigner = () => {
) : (
<Input
unstyle
className="w-full rounded-md border border-slate-300 bg-white px-2 py-2 text-[10px] disabled:opacity-50 dark:border-slate-700 dark:bg-slate-900"
className="w-full rounded-md border border-slate-300 bg-white px-2 py-2 text-[10px] disabled:opacity-50 dark:border-gray-700 dark:bg-gray-900"
disabled={!column}
placeholder={translate('::App.DeveloperKitComponentDesigner.NoDefaultValue')}
type={defaultInputType}
@ -4831,7 +4831,7 @@ const VisualComponentDesigner = () => {
{SQL_DEFAULT_VALUE_TOKENS.map((token) => (
<button
key={token}
className="rounded border border-slate-300 px-1.5 py-0.5 font-mono text-[9px] text-slate-600 hover:border-sky-400 hover:text-sky-700 disabled:opacity-40 dark:border-slate-700 dark:text-slate-300"
className="rounded border border-slate-300 px-1.5 py-0.5 font-mono text-[9px] text-slate-600 hover:border-sky-400 hover:text-sky-700 disabled:opacity-40 dark:border-gray-700 dark:text-gray-300"
disabled={!column || sqlRecordProperty === 'checked'}
type="button"
onClick={() => updateSelectedProp(SQL_DEFAULT_VALUE_PROP, `@${token}`)}
@ -4887,7 +4887,7 @@ const VisualComponentDesigner = () => {
<div className="space-y-3">
{scopeNotice}
{selectModeSelector}
<div className="rounded-lg border border-dashed border-slate-300 p-3 text-xs leading-5 text-slate-500 dark:border-slate-700">
<div className="rounded-lg border border-dashed border-slate-300 p-3 text-xs leading-5 text-slate-500 dark:border-gray-700">
{translate('::App.DeveloperKitComponentDesigner.NoSelectableGetEndpoint')}
</div>
{renderSqlRecordField()}
@ -4901,7 +4901,7 @@ const VisualComponentDesigner = () => {
{selectModeSelector}
{/* Inside a Form a record field has exactly one legal source, so
the picker is replaced by a read-only statement of that source. */}
<div className="rounded-lg border border-slate-200 p-3 dark:border-slate-800">
<div className="rounded-lg border border-slate-200 p-3 dark:border-gray-700">
<label className="block">
<span className="mb-1.5 block text-[10px] font-semibold uppercase tracking-wider text-slate-400">
{sqlScopeLocked
@ -4911,7 +4911,7 @@ const VisualComponentDesigner = () => {
: 'Endpoint'}
</span>
{sqlScopeLocked ? (
<div className="rounded-md border border-slate-200 bg-slate-50 px-2.5 py-2 text-xs text-slate-600 dark:border-slate-700 dark:bg-slate-950 dark:text-slate-300">
<div className="rounded-md border border-slate-200 bg-slate-50 px-2.5 py-2 text-xs text-slate-600 dark:border-gray-700 dark:bg-gray-950 dark:text-gray-300">
{sqlScopeSource?.name}
</div>
) : (
@ -4991,7 +4991,7 @@ const VisualComponentDesigner = () => {
{isTabularDataComponent(selectedNode?.type) &&
activeDataSample !== undefined &&
tabularItemsBinding?.sourceId && (
<div className="rounded-lg border border-slate-200 p-3 dark:border-slate-800">
<div className="rounded-lg border border-slate-200 p-3 dark:border-gray-700">
<label className="block">
<span className="mb-1 block text-[10px] font-semibold uppercase tracking-wider text-slate-400">
Koleksiyon
@ -5022,7 +5022,7 @@ const VisualComponentDesigner = () => {
)}
{!isOptionDataComponent(selectedNode?.type) && activeBindingSample !== undefined && (
<div className="rounded-lg border border-slate-200 p-3 dark:border-slate-800">
<div className="rounded-lg border border-slate-200 p-3 dark:border-gray-700">
<div className="mb-2 flex items-center justify-between">
<span className="text-[10px] font-semibold uppercase tracking-wider text-slate-400">
{translate('::App.DeveloperKitComponentDesigner.AvailableFields')}
@ -5069,7 +5069,7 @@ const VisualComponentDesigner = () => {
activeDataFields.map((field) => (
<label
key={field.path}
className="grid cursor-pointer grid-cols-[auto_1fr_auto] gap-2 rounded bg-slate-50 px-2 py-1.5 dark:bg-slate-950"
className="grid cursor-pointer grid-cols-[auto_1fr_auto] gap-2 rounded bg-slate-50 px-2 py-1.5 dark:bg-gray-950"
title={field.preview}
>
{isTabularDataComponent(selectedNode?.type) &&
@ -5111,7 +5111,7 @@ const VisualComponentDesigner = () => {
{translate('::App.DeveloperKitComponentDesigner.PropertyBindings')}
</div>
{!orderedBindableProperties.length ? (
<p className="rounded-lg border border-dashed border-slate-300 p-3 text-xs leading-5 text-slate-500 dark:border-slate-700">
<p className="rounded-lg border border-dashed border-slate-300 p-3 text-xs leading-5 text-slate-500 dark:border-gray-700">
{translate('::App.DeveloperKitComponentDesigner.NoBindableProperty')}
</p>
) : (
@ -5163,9 +5163,9 @@ const VisualComponentDesigner = () => {
return (
<label
key={property.name}
className="mb-2 block rounded-lg border border-slate-200 p-2 dark:border-slate-800"
className="mb-2 block rounded-lg border border-slate-200 p-2 dark:border-gray-700"
>
<span className="mb-1.5 flex items-center justify-between text-[11px] font-medium text-slate-600 dark:text-slate-300">
<span className="mb-1.5 flex items-center justify-between text-[11px] font-medium text-slate-600 dark:text-gray-300">
<span>{property.name}</span>
<span className="text-[9px] font-normal text-slate-400">
{property.tsType || property.type}
@ -5232,7 +5232,7 @@ const VisualComponentDesigner = () => {
nodes.map((node) => (
<React.Fragment key={node.id}>
<button
className={`flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-xs ${selectedId === node.id ? 'bg-sky-100 font-semibold text-sky-700 dark:bg-sky-950 dark:text-sky-300' : 'text-slate-600 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-800'}`}
className={`flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-xs ${selectedId === node.id ? 'bg-sky-100 font-semibold text-sky-700 dark:bg-sky-950 dark:text-sky-300' : 'text-slate-600 hover:bg-slate-100 dark:text-gray-300 dark:hover:bg-gray-800'}`}
style={{ paddingLeft: 8 + depth * 14 }}
type="button"
onClick={() => selectDesignerNode(node.id)}
@ -5344,12 +5344,12 @@ const VisualComponentDesigner = () => {
: ''
return (
<div className="flex h-[calc(100vh-72px)] min-h-[720px] flex-col overflow-hidden rounded-xl border border-slate-200 bg-slate-100 shadow-sm dark:border-slate-800 dark:bg-slate-950">
<div className="flex h-[calc(100vh-72px)] min-h-[720px] flex-col overflow-hidden rounded-xl border border-slate-200 bg-slate-100 shadow-sm dark:border-gray-700 dark:bg-gray-800">
{pageHelmet}
<header className="flex h-16 shrink-0 items-center justify-between border-b border-slate-200 bg-white px-4 dark:border-slate-800 dark:bg-slate-900">
<header className="flex h-16 shrink-0 items-center justify-between border-b border-slate-200 bg-white px-4 dark:border-gray-700 dark:bg-gray-900">
<div className="flex min-w-0 items-center gap-3">
<Link
className="rounded-md p-2 text-slate-500 hover:bg-slate-100 dark:hover:bg-slate-800"
className="rounded-md p-2 text-slate-500 hover:bg-slate-100 dark:hover:bg-gray-800"
title={translate('::App.DeveloperKitComponentEditor.Back')}
to={ROUTES_ENUM.protected.saas.developerKit.components}
>
@ -5393,7 +5393,7 @@ const VisualComponentDesigner = () => {
variant="plain"
onClick={redo}
/>
<div className="mx-1 h-6 w-px bg-slate-200 dark:bg-slate-700" />
<div className="mx-1 h-6 w-px bg-slate-200 dark:bg-gray-700" />
<Button
icon={<FaSync />}
size="sm"
@ -5435,8 +5435,8 @@ const VisualComponentDesigner = () => {
<div className="flex min-h-0 flex-1">
{workspaceTab === 'design' && (
<aside className="flex w-80 shrink-0 flex-col border-r border-slate-200 bg-white dark:border-slate-800 dark:bg-slate-900">
<div className="border-b border-slate-200 p-3 dark:border-slate-800">
<aside className="flex w-80 shrink-0 flex-col border-r border-slate-200 bg-white dark:border-gray-700 dark:bg-gray-900">
<div className="border-b border-slate-200 p-3 dark:border-gray-700">
<div className="mb-2 flex items-center gap-2 text-xs font-semibold uppercase tracking-wider text-slate-500">
<FaPlus /> Toolbox
</div>
@ -5444,7 +5444,7 @@ const VisualComponentDesigner = () => {
<FaSearch className="absolute left-2.5 top-2.5 text-xs text-slate-400" />
<Input
unstyle
className="w-full rounded-md border border-slate-200 bg-slate-50 py-2 pl-8 pr-2 text-xs outline-none focus:border-sky-400 dark:border-slate-700 dark:bg-slate-950"
className="w-full rounded-md border border-slate-200 bg-slate-50 py-2 pl-8 pr-2 text-xs outline-none focus:border-sky-400 dark:border-gray-700 dark:bg-gray-950"
placeholder={translate('::App.DeveloperKitComponentDesigner.SearchComponent')}
value={search}
onChange={(event) => setSearch(event.target.value)}
@ -5456,7 +5456,7 @@ const VisualComponentDesigner = () => {
<section key={group} className="mb-5">
<button
aria-expanded={expandedToolboxGroups[group] || Boolean(search.trim())}
className="mb-2 flex w-full items-center gap-2 rounded bg-slate-100 px-2 py-1.5 text-left text-[10px] font-bold uppercase tracking-widest text-slate-600 dark:bg-slate-800 dark:text-slate-300"
className="mb-2 flex w-full items-center gap-2 rounded bg-slate-100 px-2 py-1.5 text-left text-[10px] font-bold uppercase tracking-widest text-slate-600 dark:bg-gray-800 dark:text-gray-300"
type="button"
onClick={() =>
setExpandedToolboxGroups((current) => ({
@ -5471,7 +5471,7 @@ const VisualComponentDesigner = () => {
<FaChevronRight className="shrink-0" />
)}
<span>{TOOLBOX_GROUP_LABELS[group]}</span>
<span className="ml-auto rounded bg-white/70 px-1.5 py-0.5 text-[9px] font-medium tracking-normal dark:bg-slate-900/60">
<span className="ml-auto rounded bg-white/70 px-1.5 py-0.5 text-[9px] font-medium tracking-normal dark:bg-gray-900/60">
{definitions.length}
</span>
</button>
@ -5481,10 +5481,10 @@ const VisualComponentDesigner = () => {
<button
key={`${definition.toolboxGroup}-${definition.name}`}
draggable
className={`group min-w-0 rounded-lg border bg-white px-2 py-2.5 text-left transition hover:border-sky-400 hover:bg-sky-50 dark:bg-slate-900 dark:hover:border-sky-600 dark:hover:bg-sky-950 ${
className={`group min-w-0 rounded-lg border bg-white px-2 py-2.5 text-left transition hover:border-sky-400 hover:bg-sky-50 dark:bg-gray-900 dark:hover:border-sky-600 dark:hover:bg-sky-950 ${
activeDrag?.source === 'library' && activeDrag.name === definition.name
? 'border-sky-500 ring-2 ring-sky-300 dark:border-sky-500'
: 'border-slate-200 dark:border-slate-700'
: 'border-slate-200 dark:border-gray-700'
}`}
title={
definition.acceptsChildren
@ -5509,10 +5509,10 @@ const VisualComponentDesigner = () => {
}}
onDragEnd={() => endDesignerDrag()}
>
<div className="mb-1 flex h-6 w-6 items-center justify-center rounded bg-slate-100 text-[10px] font-bold text-slate-500 group-hover:bg-sky-100 group-hover:text-sky-600 dark:bg-slate-800">
<div className="mb-1 flex h-6 w-6 items-center justify-center rounded bg-slate-100 text-[10px] font-bold text-slate-500 group-hover:bg-sky-100 group-hover:text-sky-600 dark:bg-gray-800">
{definition.name.slice(0, 2).toUpperCase()}
</div>
<div className="truncate text-[11px] font-medium text-slate-700 dark:text-slate-200">
<div className="truncate text-[11px] font-medium text-slate-700 dark:text-gray-200">
{definition.name}
</div>
</button>
@ -5526,7 +5526,7 @@ const VisualComponentDesigner = () => {
)}
<main className="flex min-w-0 flex-1 flex-col">
<div className="flex h-12 shrink-0 items-center justify-between border-b border-slate-200 bg-white px-3 dark:border-slate-800 dark:bg-slate-900">
<div className="flex h-12 shrink-0 items-center justify-between border-b border-slate-200 bg-white px-3 dark:border-gray-700 dark:bg-gray-900">
<div className="flex h-full items-center gap-1">
{(
[
@ -5549,7 +5549,7 @@ const VisualComponentDesigner = () => {
</div>
<div className="flex items-center gap-2">
{workspaceTab === 'design' && (
<div className="flex items-center rounded-md border border-slate-200 p-0.5 dark:border-slate-700">
<div className="flex items-center rounded-md border border-slate-200 p-0.5 dark:border-gray-700">
{(
[
['responsive', <FaDesktop key="r" />, 'Responsive'],
@ -5560,7 +5560,7 @@ const VisualComponentDesigner = () => {
).map(([width, icon, label]) => (
<button
key={width}
className={`rounded p-1.5 ${document.canvas.width === width ? 'bg-sky-100 text-sky-600 dark:bg-sky-950' : 'text-slate-400 hover:text-slate-700 dark:hover:text-slate-200'}`}
className={`rounded p-1.5 ${document.canvas.width === width ? 'bg-sky-100 text-sky-600 dark:bg-sky-950' : 'text-slate-400 hover:text-slate-700 dark:hover:text-gray-200'}`}
title={label}
type="button"
onClick={() =>
@ -5581,7 +5581,7 @@ const VisualComponentDesigner = () => {
className={`flex items-center gap-1.5 rounded-md border px-2.5 py-1.5 text-xs font-medium ${
scriptApiOpen
? 'border-sky-500 bg-sky-50 text-sky-600 dark:bg-sky-950'
: 'border-slate-200 text-slate-500 hover:text-slate-800 dark:border-slate-700 dark:hover:text-white'
: 'border-slate-200 text-slate-500 hover:text-slate-800 dark:border-gray-700 dark:hover:text-white'
}`}
title={translate('::App.DeveloperKitComponentDesigner.ScriptApiExamples')}
type="button"
@ -5596,28 +5596,24 @@ const VisualComponentDesigner = () => {
</div>
</div>
<div className="min-h-0 flex-1 overflow-auto bg-slate-200 p-5 dark:bg-slate-950">
<div className="min-h-0 flex-1 overflow-auto bg-slate-200 p-5 dark:bg-gray-950">
{workspaceTab === 'design' ? (
<div
className="mx-auto min-h-full overflow-hidden rounded-lg bg-white text-slate-900 shadow-lg transition-[width] duration-200 dark:bg-slate-900 dark:text-slate-100"
className="mx-auto min-h-full overflow-hidden rounded-lg bg-white text-slate-900 shadow-lg transition-[width] duration-200 dark:bg-gray-900 dark:text-gray-100"
style={{ width: CANVAS_WIDTHS[document.canvas.width], maxWidth: '100%' }}
>
{canvas}
</div>
) : workspaceTab === 'data' ? (
<div className="w-full space-y-4">
<div className="flex items-center justify-between rounded-lg border border-slate-200 bg-white p-4 shadow-sm dark:border-slate-800 dark:bg-slate-900">
<div className="flex items-center justify-between rounded-lg border border-slate-200 bg-white p-4 shadow-sm dark:border-gray-700 dark:bg-gray-900">
<div className="min-w-0">
<h2 className="flex items-center gap-2 font-semibold text-slate-800 dark:text-white">
<FaDatabase className="text-sky-600" /> Data sources
</h2>
<p className="mt-1 text-xs text-slate-500">
{translate('::App.DeveloperKitComponentDesigner.ApiAuthHint')}
</p>
<p className="rounded-lg border border-sky-200 bg-sky-50 p-3 text-xs text-sky-900 dark:border-sky-900 dark:bg-sky-950 dark:text-sky-200">
{translate('::App.DeveloperKitComponentDesigner.GetPostHint')}
</p>
<p className="rounded-lg border border-sky-200 bg-sky-50 p-3 text-xs text-sky-900 dark:border-sky-900 dark:bg-sky-950 dark:text-sky-200">
{translate('::App.DeveloperKitComponentDesigner.ApiAuthHint')}{' '}
{translate('::App.DeveloperKitComponentDesigner.GetPostHint')}{' '}
{translate('::App.DeveloperKitComponentDesigner.EndpointSettingsHint')}
</p>
</div>
@ -5628,7 +5624,7 @@ const VisualComponentDesigner = () => {
</div>
</div>
{(dataSourceCatalog.length > 0 || endpointCatalogError) && (
<div className="rounded-lg border border-slate-200 bg-white p-4 shadow-sm dark:border-slate-800 dark:bg-slate-900">
<div className="rounded-lg border border-slate-200 bg-white p-4 shadow-sm dark:border-gray-700 dark:bg-gray-900">
<div className="mb-3">
<h3 className="text-sm font-semibold text-slate-800 dark:text-white">
{translate('::App.DeveloperKitComponentDesigner.AvailableSources')}
@ -5669,8 +5665,8 @@ const VisualComponentDesigner = () => {
/>
</div>
) : (
<div className="flex min-h-[620px] w-full overflow-hidden rounded-lg border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900">
<div className="w-72 shrink-0 border-r border-slate-200 bg-slate-50 p-3 dark:border-slate-800 dark:bg-slate-950">
<div className="flex min-h-[620px] w-full overflow-hidden rounded-lg border border-slate-200 bg-white shadow-sm dark:border-gray-700 dark:bg-gray-900">
<div className="w-72 shrink-0 border-r border-slate-200 bg-slate-50 p-3 dark:border-gray-700 dark:bg-gray-950">
<div className="mb-3">
<h2 className="flex items-center gap-2 font-semibold text-slate-800 dark:text-white">
<FaBolt className="text-amber-500" /> Event workspace
@ -5684,7 +5680,7 @@ const VisualComponentDesigner = () => {
javascriptEventTargets.map((target) => (
<button
key={target.node.id}
className={`flex w-full items-center gap-2 rounded-md border px-2.5 py-2 text-left transition ${activeJavaScriptTarget?.node.id === target.node.id ? 'border-amber-400 bg-amber-50 text-amber-800 dark:bg-amber-950 dark:text-amber-200' : 'border-transparent text-slate-600 hover:border-slate-200 hover:bg-white dark:text-slate-300 dark:hover:border-slate-700 dark:hover:bg-slate-900'}`}
className={`flex w-full items-center gap-2 rounded-md border px-2.5 py-2 text-left transition ${activeJavaScriptTarget?.node.id === target.node.id ? 'border-amber-400 bg-amber-50 text-amber-800 dark:bg-amber-950 dark:text-amber-200' : 'border-transparent text-slate-600 hover:border-slate-200 hover:bg-white dark:text-gray-300 dark:hover:border-gray-700 dark:hover:bg-gray-900'}`}
type="button"
onClick={() => setSelectedId(target.node.id)}
>
@ -5692,20 +5688,20 @@ const VisualComponentDesigner = () => {
<span className="min-w-0 flex-1 truncate text-xs font-semibold">
{target.node.type}
</span>
<span className="rounded bg-slate-200 px-1.5 py-0.5 text-[9px] dark:bg-slate-800">
<span className="rounded bg-slate-200 px-1.5 py-0.5 text-[9px] dark:bg-gray-800">
{target.eventNames.length}
</span>
</button>
))
) : (
<p className="rounded-md border border-dashed border-slate-300 p-3 text-xs leading-5 text-slate-500 dark:border-slate-700">
<p className="rounded-md border border-dashed border-slate-300 p-3 text-xs leading-5 text-slate-500 dark:border-gray-700">
{translate('::App.DeveloperKitComponentDesigner.NoEventComponent')}
</p>
)}
</div>
{/* Every component on the page is addressable from any script,
which is what makes cross component behaviour possible. */}
<div className="mt-4 border-t border-slate-200 pt-3 dark:border-slate-800">
<div className="mt-4 border-t border-slate-200 pt-3 dark:border-gray-700">
<h3 className="text-[11px] font-semibold uppercase tracking-wider text-slate-400">
Referanslar
</h3>
@ -5719,7 +5715,7 @@ const VisualComponentDesigner = () => {
{[...documentRefs.entries()].map(([ref, node]) => (
<button
key={ref}
className="flex w-full items-center gap-2 rounded border border-transparent px-2 py-1 text-left hover:border-slate-200 hover:bg-white dark:hover:border-slate-700 dark:hover:bg-slate-900"
className="flex w-full items-center gap-2 rounded border border-transparent px-2 py-1 text-left hover:border-slate-200 hover:bg-white dark:hover:border-gray-700 dark:hover:bg-gray-900"
title={translate('::App.DeveloperKitComponentDesigner.CopyRefName')}
type="button"
onClick={() => copyRefName(ref)}
@ -5740,7 +5736,7 @@ const VisualComponentDesigner = () => {
</div>
<div className="min-w-0 flex-1 overflow-y-auto p-5">
<div className="mb-5 rounded-lg border border-slate-200 p-4 dark:border-slate-800">
<div className="mb-5 rounded-lg border border-slate-200 p-4 dark:border-gray-700">
<div className="mb-2 flex items-center justify-between">
<div>
<h3 className="text-sm font-semibold text-slate-800 dark:text-white">
@ -5791,10 +5787,10 @@ const VisualComponentDesigner = () => {
{activeJavaScriptTarget.eventNames.map((eventName) => (
<label
key={eventName}
className={`overflow-hidden rounded-lg border dark:border-slate-800 ${eventName === activeEventName ? 'border-amber-400' : 'border-slate-200'}`}
className={`overflow-hidden rounded-lg border dark:border-gray-700 ${eventName === activeEventName ? 'border-amber-400' : 'border-slate-200'}`}
onFocus={() => setFocusedEventName(eventName)}
>
<span className="flex items-center justify-between bg-slate-50 px-3 py-2 text-xs font-semibold text-slate-700 dark:bg-slate-950 dark:text-slate-200">
<span className="flex items-center justify-between bg-slate-50 px-3 py-2 text-xs font-semibold text-slate-700 dark:bg-gray-950 dark:text-gray-200">
<span
className="min-w-0 truncate"
title={
@ -5877,7 +5873,7 @@ const VisualComponentDesigner = () => {
</div>
</div>
) : (
<div className="rounded-lg border border-dashed border-slate-300 p-8 text-center text-sm text-slate-500 dark:border-slate-700">
<div className="rounded-lg border border-dashed border-slate-300 p-8 text-center text-sm text-slate-500 dark:border-gray-700">
{translate('::App.DeveloperKitComponentDesigner.NoEditableEvent')}
</div>
)}
@ -5890,9 +5886,9 @@ const VisualComponentDesigner = () => {
{workspaceTab === 'design' && (
<aside
data-property-panel
className="flex w-72 shrink-0 flex-col border-l border-slate-200 bg-white dark:border-slate-800 dark:bg-slate-900"
className="flex w-72 shrink-0 flex-col border-l border-slate-200 bg-white dark:border-gray-700 dark:bg-gray-900"
>
<div className="flex h-12 shrink-0 border-b border-slate-200 dark:border-slate-800">
<div className="flex h-12 shrink-0 border-b border-slate-200 dark:border-gray-700">
{(
[
['properties', <FaSlidersH key="p" />, 'Properties'],
@ -5902,7 +5898,7 @@ const VisualComponentDesigner = () => {
).map(([tab, icon, label]) => (
<button
key={tab}
className={`flex flex-1 items-center justify-center gap-1 border-b-2 text-[11px] ${tab === 'data' && !selectedIsDataComponent ? 'cursor-not-allowed border-transparent text-slate-300 dark:text-slate-700' : inspectorTab === tab ? 'border-sky-500 text-sky-600' : 'border-transparent text-slate-500'}`}
className={`flex flex-1 items-center justify-center gap-1 border-b-2 text-[11px] ${tab === 'data' && !selectedIsDataComponent ? 'cursor-not-allowed border-transparent text-slate-300 dark:text-gray-700' : inspectorTab === tab ? 'border-sky-500 text-sky-600' : 'border-transparent text-slate-500'}`}
disabled={tab === 'data' && !selectedIsDataComponent}
title={
tab === 'data' && !selectedIsDataComponent
@ -5932,7 +5928,7 @@ const VisualComponentDesigner = () => {
</div>
) : selectedNode ? (
<>
<div className="mb-4 rounded-lg bg-slate-50 p-3 dark:bg-slate-950">
<div className="mb-4 rounded-lg bg-slate-50 p-3 dark:bg-gray-950">
<div className="text-[10px] uppercase tracking-wider text-slate-400">
Selected component
</div>
@ -5950,7 +5946,7 @@ const VisualComponentDesigner = () => {
</button>
</div>
<code className="text-[10px] text-slate-400">{selectedNode.id}</code>
<div className="mt-2 border-t border-slate-200 pt-2 dark:border-slate-800">
<div className="mt-2 border-t border-slate-200 pt-2 dark:border-gray-700">
<span className="flex items-center justify-between text-[10px] font-semibold uppercase tracking-wider text-slate-400">
Ref
<button
@ -5964,7 +5960,7 @@ const VisualComponentDesigner = () => {
</span>
<Input
unstyle
className="mt-1 w-full rounded border border-slate-300 bg-white px-2 py-1 font-mono text-[11px] text-slate-700 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-200"
className="mt-1 w-full rounded border border-slate-300 bg-white px-2 py-1 font-mono text-[11px] text-slate-700 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-200"
spellCheck={false}
value={refDraft}
onBlur={commitRefRename}
@ -5982,7 +5978,7 @@ const VisualComponentDesigner = () => {
)}
</div>
{selectedDefinition?.interfaceName && (
<div className="mt-2 border-t border-slate-200 pt-2 dark:border-slate-800">
<div className="mt-2 border-t border-slate-200 pt-2 dark:border-gray-700">
<div className="text-[10px] font-semibold text-sky-600">
{selectedDefinition.interfaceName}
</div>
@ -6040,9 +6036,9 @@ const VisualComponentDesigner = () => {
return (
<div
key={property.name}
className="group/property mb-4 rounded-md border border-slate-200 p-2 dark:border-slate-800"
className="group/property mb-4 rounded-md border border-slate-200 p-2 dark:border-gray-700"
>
<span className="mb-1.5 flex items-center justify-between text-[11px] font-medium text-slate-600 dark:text-slate-300">
<span className="mb-1.5 flex items-center justify-between text-[11px] font-medium text-slate-600 dark:text-gray-300">
<span>
{property.name}
{property.required ? (
@ -6064,7 +6060,7 @@ const VisualComponentDesigner = () => {
>
<FaBolt />
</button>
<span className="pointer-events-none absolute right-0 top-full z-20 mt-1 hidden w-56 rounded-md border border-slate-200 bg-white p-2 text-left font-mono text-[9px] leading-4 text-sky-700 shadow-lg group-hover/snippet:block dark:border-slate-700 dark:bg-slate-950 dark:text-sky-300">
<span className="pointer-events-none absolute right-0 top-full z-20 mt-1 hidden w-56 rounded-md border border-slate-200 bg-white p-2 text-left font-mono text-[9px] leading-4 text-sky-700 shadow-lg group-hover/snippet:block dark:border-gray-700 dark:bg-gray-950 dark:text-sky-300">
{snippet}
<span className="mt-1 block font-sans text-[9px] text-slate-400">
{translate(
@ -6108,7 +6104,7 @@ const VisualComponentDesigner = () => {
</>
) : (
<div className="space-y-4">
<div className="rounded-lg border border-dashed border-slate-300 p-5 text-center dark:border-slate-700">
<div className="rounded-lg border border-dashed border-slate-300 p-5 text-center dark:border-gray-700">
<FaMousePointer className="mx-auto mb-2 text-slate-400" />
<p className="text-xs text-slate-500">
{translate('::App.DeveloperKitComponentDesigner.SelectComponentForProps')}
@ -6117,7 +6113,7 @@ const VisualComponentDesigner = () => {
</div>
)}
</div>
<div className="border-t border-slate-200 bg-slate-50 px-3 py-2 text-[10px] text-slate-500 dark:border-slate-800 dark:bg-slate-950">
<div className="border-t border-slate-200 bg-slate-50 px-3 py-2 text-[10px] text-slate-500 dark:border-gray-700 dark:bg-gray-950">
<span className="font-semibold">{document.nodes.length}</span> root component ·{' '}
<span className="font-semibold">Ctrl+S</span> save ·{' '}
<span className="font-semibold">Del</span> delete
@ -6131,8 +6127,8 @@ const VisualComponentDesigner = () => {
than a dialog on purpose: no backdrop, so the editor underneath stays
usable while an example is being read or pasted. */}
{scriptApiOpen && (
<aside className="fixed inset-y-0 right-0 z-[90] flex w-full max-w-md flex-col border-l border-slate-200 bg-white shadow-2xl dark:border-slate-700 dark:bg-slate-900">
<div className="flex shrink-0 items-start justify-between gap-3 border-b border-slate-200 px-4 py-3 dark:border-slate-800">
<aside className="fixed inset-y-0 right-0 z-[90] flex w-full max-w-md flex-col border-l border-slate-200 bg-white shadow-2xl dark:border-gray-700 dark:bg-gray-900">
<div className="flex shrink-0 items-start justify-between gap-3 border-b border-slate-200 px-4 py-3 dark:border-gray-700">
<div className="min-w-0">
<h2 className="flex items-center gap-2 text-sm font-semibold text-slate-800 dark:text-white">
<FaBolt className="text-amber-500" /> Script API
@ -6147,7 +6143,7 @@ const VisualComponentDesigner = () => {
</code>
</div>
<button
className="rounded p-1.5 text-slate-400 hover:bg-slate-100 hover:text-slate-700 dark:hover:bg-slate-800 dark:hover:text-white"
className="rounded p-1.5 text-slate-400 hover:bg-slate-100 hover:text-slate-700 dark:hover:bg-gray-800 dark:hover:text-white"
title={translate('::App.Platform.CloseWithEsc')}
type="button"
onClick={() => setScriptApiOpen(false)}
@ -6155,10 +6151,10 @@ const VisualComponentDesigner = () => {
×
</button>
</div>
<div className="shrink-0 border-b border-slate-200 px-4 py-3 dark:border-slate-800">
<div className="shrink-0 border-b border-slate-200 px-4 py-3 dark:border-gray-700">
<Input
unstyle
className="w-full rounded-md border border-slate-300 bg-white px-3 py-1.5 text-xs text-slate-700 dark:border-slate-700 dark:bg-slate-950 dark:text-slate-200"
className="w-full rounded-md border border-slate-300 bg-white px-3 py-1.5 text-xs text-slate-700 dark:border-gray-700 dark:bg-gray-950 dark:text-gray-200"
placeholder={translate('::App.DeveloperKitComponentDesigner.SearchExample')}
value={scriptApiSearch}
onChange={(event) => setScriptApiSearch(event.target.value)}
@ -6190,11 +6186,11 @@ const VisualComponentDesigner = () => {
return (
<div
key={recipe.title}
className="overflow-hidden rounded-md border border-slate-200 dark:border-slate-800"
className="overflow-hidden rounded-md border border-slate-200 dark:border-gray-700"
>
<div className="flex items-start justify-between gap-2 px-3 pt-2">
<div className="min-w-0">
<div className="text-xs font-semibold text-slate-700 dark:text-slate-200">
<div className="text-xs font-semibold text-slate-700 dark:text-gray-200">
{translate('::' + recipe.title)}
</div>
<p className="text-[10px] leading-4 text-slate-400">
@ -6240,7 +6236,7 @@ const VisualComponentDesigner = () => {
)
})}
{!filteredScriptRecipes.length && (
<p className="rounded-md border border-dashed border-slate-300 p-4 text-center text-xs text-slate-500 dark:border-slate-700">
<p className="rounded-md border border-dashed border-slate-300 p-4 text-center text-xs text-slate-500 dark:border-gray-700">
{translate('::App.DeveloperKitComponentDesigner.NoMatchingExample')}
</p>
)}
@ -6250,8 +6246,8 @@ const VisualComponentDesigner = () => {
{catalogSourceEditor && (
<div className="fixed inset-0 z-[100] flex items-center justify-center bg-slate-950/70 p-4">
<div className="w-full max-w-2xl overflow-hidden rounded-xl border border-slate-200 bg-white shadow-2xl dark:border-slate-700 dark:bg-slate-900">
<div className="flex items-start justify-between border-b border-slate-200 px-5 py-4 dark:border-slate-700">
<div className="w-full max-w-2xl overflow-hidden rounded-xl border border-slate-200 bg-white shadow-2xl dark:border-gray-700 dark:bg-gray-900">
<div className="flex items-start justify-between border-b border-slate-200 px-5 py-4 dark:border-gray-700">
<div>
<h2 className="text-sm font-semibold text-slate-800 dark:text-white">
{translate(
@ -6266,7 +6262,7 @@ const VisualComponentDesigner = () => {
</p>
</div>
<button
className="rounded p-1.5 text-slate-400 hover:bg-slate-100 hover:text-slate-700 dark:hover:bg-slate-800 dark:hover:text-white"
className="rounded p-1.5 text-slate-400 hover:bg-slate-100 hover:text-slate-700 dark:hover:bg-gray-800 dark:hover:text-white"
title={translate('::App.Platform.Close')}
type="button"
onClick={() => {
@ -6286,7 +6282,7 @@ const VisualComponentDesigner = () => {
</span>
<Input
unstyle
className="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-xs text-slate-800 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-950 dark:text-slate-100"
className="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-xs text-slate-800 outline-none focus:border-sky-500 dark:border-gray-700 dark:bg-gray-950 dark:text-gray-100"
value={catalogSourceEditor.draft.name}
onChange={(event) => updateCatalogSourceDraft({ name: event.target.value })}
/>
@ -6323,7 +6319,7 @@ const VisualComponentDesigner = () => {
</span>
<Input
unstyle
className="w-full rounded-md border border-slate-300 bg-white px-3 py-2 font-mono text-xs text-slate-800 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-950 dark:text-slate-100"
className="w-full rounded-md border border-slate-300 bg-white px-3 py-2 font-mono text-xs text-slate-800 outline-none focus:border-sky-500 dark:border-gray-700 dark:bg-gray-950 dark:text-gray-100"
placeholder="/api/app/customers"
value={catalogSourceEditor.draft.url}
onChange={(event) => updateCatalogSourceDraft({ url: event.target.value })}
@ -6338,7 +6334,7 @@ const VisualComponentDesigner = () => {
</span>
<Input
unstyle
className="w-full rounded-md border border-slate-300 bg-white px-3 py-2 font-mono text-xs text-slate-800 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-950 dark:text-slate-100"
className="w-full rounded-md border border-slate-300 bg-white px-3 py-2 font-mono text-xs text-slate-800 outline-none focus:border-sky-500 dark:border-gray-700 dark:bg-gray-950 dark:text-gray-100"
placeholder="result.items"
value={catalogSourceEditor.draft.responsePath}
onChange={(event) =>
@ -6362,7 +6358,7 @@ const VisualComponentDesigner = () => {
)}
</div>
<div className="flex justify-end gap-2 border-t border-slate-200 bg-slate-50 px-5 py-4 dark:border-slate-700 dark:bg-slate-950">
<div className="flex justify-end gap-2 border-t border-slate-200 bg-slate-50 px-5 py-4 dark:border-gray-700 dark:bg-gray-950">
<Button
size="sm"
variant="plain"

View file

@ -567,6 +567,37 @@ function hideDialogLater() {
}, NOTICE_VISIBLE_MS)
}
/**
* Kurulum oturumu sürüyor mu? Isıtma mesajlarının araya girmesini engeller
* (bkz. `handleWorkerMessage`). Geçişli kurulumda sayfa yenilendiği için
* sıfırlanması yalnızca ilk kurulum (geçişsiz) akışında gerekir.
*/
let installSessionActive = false
/** Ekranda gösterilen oturumun fazı; faz değişince sayaçlar sıfırlanır. */
let activePhase: 'install' | 'warmup' | undefined
/**
* Oturumun türü BAŞLARKEN dondurulur.
*
* Başlık ve kapatılabilirlik her ilerleme mesajında yeniden hesaplanırsa araya
* giren tek bir mesaj bile pencereyi "Downloading assets" ile "Downloading the
* new version" arasında zıplatır. Karar oturum başına bir kez verilir.
*/
let sessionTransitions = false
/**
* Oturum değiştiğinde birikmiş liste/bayt bilgisi taşınmaz: iki oturumun dosya
* index'leri ayrı uzaylardadır, birleştirilirse satırlar birbirinin üzerine
* yazılır ve ilerleme yanlış hesaplanır.
*/
function startSession(phase: 'install' | 'warmup' | undefined) {
if (activePhase === phase) return
activePhase = phase
resetRate()
setState({ files: [], loadedBytes: 0, totalBytes: 0, percent: 0, secondsRemaining: undefined })
}
function handleWorkerMessage(event: MessageEvent) {
const data = event.data as SwProgressMessage | undefined
if (!data?.type) return
@ -586,11 +617,20 @@ function handleWorkerMessage(event: MessageEvent) {
*/
const transitions = controlled && data.phase !== 'warmup'
// Isıtma kurulumla aynı anda koşabiliyor (eski aktif worker ısıtırken yenisi
// kuruluyor). İki oturumun dosya listeleri ayrı index uzayında olduğu için
// birleşince satırlar tekrarlanıyor ve toplam boyut aşıyordu; geçiş sürecinde
// ısıtma mesajları yok sayılır.
if (data.phase === 'warmup' && (activating || state.status === 'ready')) return
/**
* Kurulum başladığı anda ısıtma mesajları tamamen susturulur.
*
* Deploy sonrası iki oturum GERÇEKTEN aynı anda koşuyor: eski aktif worker
* `ensureWarmup` ile eksik bileşenleri indirirken yeni worker kuruluyor. İki
* oturumun dosya listeleri ayrı index uzayında olduğu için birleşince satırlar
* birbirinin üzerine yazılıyor, toplam boyut şişiyor ve pencere her tick'te
* "Downloading assets" ile "Downloading the new version" arasında gidip
* geliyordu. Kurulum kazanır: çalışan sürümü değiştiren tek oturum odur.
*/
if (data.phase !== 'warmup') installSessionActive = true
if (data.phase === 'warmup' && (installSessionActive || activating || state.status === 'ready')) {
return
}
const totals = { loadedBytes: data.loadedBytes, totalBytes: data.totalBytes }
@ -604,37 +644,50 @@ function handleWorkerMessage(event: MessageEvent) {
// Geçişli akış kapatılamaz; önceki kapatma tercihi burada geçersizleşir.
if (transitions) forceDialog()
void showUpdateDialog('downloading')
resetRate()
setState({ percent: 0 })
activePhase = undefined
startSession(data.phase)
sessionTransitions = transitions
recalculate(toFiles(data.files), totals, {
status: 'downloading',
visible: dialogVisible(),
reusedCount: data.reusedCount ?? 0,
secondsLeft: undefined,
dismissible: !transitions,
dismissible: !sessionTransitions,
})
break
case 'SW_PRECACHE_PROGRESS':
// Durum isteği bir yarışa takılıp cevapsız kalırsa pencere burada açılır.
if (!dialogMountRequested) void showUpdateDialog('downloading')
// START/STATE'i kaçırmış olabiliriz; faz değiştiyse önceki oturum atılır.
if (activePhase !== data.phase) {
startSession(data.phase)
sessionTransitions = transitions
}
recalculate(mergeFiles(toFiles(data.updates)), totals, {
status: 'downloading',
visible: dialogVisible(),
dismissible: !transitions,
dismissible: !sessionTransitions,
})
break
case 'SW_PRECACHE_DONE':
// Geçişsiz kurulum bittiğinde ısıtma yeniden söz sahibi olabilir.
if (!sessionTransitions) {
installSessionActive = false
activePhase = undefined
}
recalculate(keepSeq(toFiles(data.files)), totals, {
// Yeni sürüme geçilecekse 'ready'; ısıtmada iş bitti.
status: transitions ? 'ready' : 'completed',
visible: transitions || dialogVisible(),
status: sessionTransitions ? 'ready' : 'completed',
visible: sessionTransitions || dialogVisible(),
percent: 100,
})
if (transitions) scheduleActivationHandoff()
if (sessionTransitions) scheduleActivationHandoff()
else hideDialogLater()
break
case 'SW_PRECACHE_ERROR':
if (!transitions) {
if (!sessionTransitions) {
installSessionActive = false
activePhase = undefined
// Çalışan sürüm etkilenmedi; kullanıcıyı yeniden yükleme döngüsüne sokma.
setState({
status: 'error',
@ -757,8 +810,9 @@ function requestPrecacheState(reg: ServiceWorkerRegistration) {
* Worker eksik bir şey yoksa hiçbir mesaj yollamaz, pencere de ılmaz.
*/
function requestWarmup(reg: ServiceWorkerRegistration) {
// Kurulum sürüyorsa zaten indiriliyor; ikinci bir oturum başlatma.
if (reg.installing) return
// Kurulum sürüyor ya da geçişi bekliyorsa ikinci bir oturum başlatma: iki
// oturum hem bant genişliği için yarışır hem de pencerede iç içe geçer.
if (reg.installing || reg.waiting || installSessionActive) return
const worker = reg.active ?? navigator.serviceWorker.controller
worker?.postMessage({ type: 'ENSURE_WARMUP' })
}