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. - 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 ## 2. Platform Scope
@ -116,7 +126,7 @@ Capabilities:
- Query - Query
- WebService - WebService
- Cascading lookup parent-child behavior - 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 - Conditional formatting and style injection
- Grid state save/load/reset - Grid state save/load/reset
- User filter save/apply/delete flows - User filter save/apply/delete flows
@ -146,6 +156,34 @@ Supported editor types:
- dxTextArea - dxTextArea
- dxTextBox - 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 ## 8. Dynamic Form System
@ -172,6 +210,8 @@ Supported component families:
- TreeList / Tree view - TreeList / Tree view
- SubForm tabs (List, Tree, Gantt, Scheduler, Form, Chart) - SubForm tabs (List, Tree, Gantt, Scheduler, Form, Chart)
- Widget Group (dashboard KPI cards) - 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: Runtime UI capabilities:
@ -212,6 +252,47 @@ If user requests custom code explicitly:
- Provide a warning that low-code path is preferred. - Provide a warning that low-code path is preferred.
- Offer configuration-first alternative first. - 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 ## 11. Integrations
@ -540,13 +621,32 @@ When in doubt, AI must choose the path that preserves:
## 31. Seeder-Driven Low-Code Development Guide (Authoritative) ## 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 **A. Repository seeds (shipped with code)** — `api/src/Sozsoft.Platform.DbMigrator/Migrations/`.
- api/src/Sozsoft.Platform.DbMigrator/Migrations/MenusData.json Platform-owned screens, menus, permissions and language texts. Changing them requires a build and
- api/src/Sozsoft.Platform.DbMigrator/Migrations/PermissionsData.json a deploy. AI must learn and teach implementation flow primarily from these assets:
- api/src/Sozsoft.Platform.DbMigrator/Migrations/HostData.json
- api/src/Sozsoft.Platform.DbMigrator/Migrations/LanguagesData.json - 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. 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. 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) ## 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. - Permission contract must be created/bound for both parent and child menu items.
- Route/menu consistency remains mandatory (`Url` and screen contract must match). - 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: 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 ## General Principles
- **Configuration First:** Always prefer platform configuration (menus, permissions, forms, localization) over custom code. - **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. - **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) │ │ ├─ DynamicRouter (route kayıtlarından React Router üretimi) │
│ ├─ ListForm runtime (Grid / Pivot / Tree / Chart / Gantt / ...) │ │ ├─ ListForm runtime (Grid / Pivot / Tree / Chart / Gantt / ...) │
│ ├─ Form runtime (DevExtreme Form, metadata ile alan üretimi)│ │ ├─ 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 | | Bileşen | Sürüm / Notlar |
| --- | --- | | --- | --- |
| React | 18.3 | | React | 19.2 |
| Build | Vite 8 + TypeScript 5.9 | | Build | Vite 8 + TypeScript 5.9 |
| UI kütüphaneleri | DevExtreme 25.2 (React), Tailwind CSS 3.4, kendi `components/ui` tasarım sistemi | | UI kütüphaneleri | DevExtreme 26.1 (React), Tailwind CSS 3.4, kendi `components/ui` tasarım sistemi |
| Routing | react-router-dom 6 (dinamik route üretimi) | | Routing | react-router-dom 7 (dinamik route üretimi) |
| State | easy-peasy (global store) + @tanstack/react-query | | State | easy-peasy (global store) + @tanstack/react-query |
| Form | Formik + Yup, DevExtreme Form | | Form | Formik + Yup, DevExtreme Form |
| Kod editörü | Monaco Editor | | Kod editörü | Monaco Editor |
@ -183,7 +183,10 @@ sozsoft-platform/
│ │ # tenants/{tenantId}/{crud,custom,wizard,sql,postgres} │ │ # tenants/{tenantId}/{crud,custom,wizard,sql,postgres}
│ │ # {sql|postgres}/{object,execute} │ │ # {sql|postgres}/{object,execute}
│ └── ai/ # AI entegrasyonu (n8n akış tanımları) │ └── 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ı ├── CLAUDE.md # Claude Code için operasyon kuralları
└── README.md └── 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: 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, 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. 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ışı ### 7.4. Alan (field) davranışı
- **Editör tipleri:** `dxTextBox`, `dxTextArea`, `dxNumberBox`, `dxDateBox`, `dxDateRangeBox`, - **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ı. - **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 - **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. 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`. - **Koşullu biçimlendirme:** `ColumnStylingJson`, `ColumnCssClass`/`ColumnCssValue`.
- **Alan bazlı yetki:** `PermissionJson` ile sütun düzeyinde okuma/yazma/dışa aktarma kontrolü. - **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 ### 7.5. İş akışı ve onay
`FormTabWorkflow` ve `views/admin/listForm/workflow/WorkflowDesigner.tsx` üzerinden görsel `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) ### 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) 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) 2. **ListFormField** kümesi (sütunlar, editörler, lookup'lar, doğrulamalar)
3. **Route** kaydı (`key`, `path`, `componentPath`, `routeType`, `authority`) 3. **Route** kaydı (`key`, `path`, `componentPath`, `routeType`, `authority`)
4. **Menu** kaydı (`ParentCode`, `Code`, `DisplayName`, `Url`, `Icon`, `RequiredPermissionName`, `Order`) 4. **Menu** kaydı (`ParentCode`, `Code`, `DisplayName`, `Url`, `Icon`, `RequiredPermissionName`, `Order`)
5. **Permission** kayıtları (`.Default`, `.Create`, `.Update`, `.Delete`, `.Export`, `.Import`, `.Note`) 5. **Permission** kayıtları (`.Default`, `.Create`, `.Update`, `.Delete`, `.Export`, `.Import`, `.Note`)
6. Gerekiyorsa **ayar/entegrasyon** bağımlılıkları 6. **Dil anahtarları** (menü, başlık, açıklama; EN + TR)
7. **Doğrulama ve geri alma** notları 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 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 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, 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). 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 ### 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). - **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 → - **İç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ı sonuç akışı; sonuçlar `ListFormImportLog` üzerinde satır bazında raporlanır. Şablon dosyası
ekran tanımından üretilir. 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 ## 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 | | 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ı. | | **Custom Endpoint** | `/admin/list/App.DeveloperKit.CustomEndpoints` | SQL veya servis tanımından REST endpoint üretimi; GET/POST/PUT/DELETE için ayrı yetki kapıları (`App.DeveloperKit.CustomEndpoints.*`) ve kullanıcı/rol/global erişim kuralları. |
| **CRUD Endpoint** | SQL Query Manager içinde | Tablo tanımından tam CRUD endpoint kümesi üretimi. Ayrı ekranı yoktur; bkz. aşağıdaki not. | | **CRUD Endpoint** | SQL Query Manager içinde | Tablo tanımından tam CRUD endpoint kümesi üretimi. Ayrı ekranı yoktur; bkz. aşağıdaki not. |
| **Dynamic Service** | `/admin/developerkit/dynamic-services` | C# kodunu tarayıcıda yazıp Roslyn ile derleme (`TestCompile`), yayınlama (`Publish`) ve çalışan uygulamaya controller olarak kaydetme. Yetkiler: Create/Edit/Delete/Manage/TestCompile/Publish/ViewCode. | | **Dynamic Service** | `/admin/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`). | | **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` > **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 > 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 > `LastModifierId`, `IsDeleted`, `DeletionTime`, `DeleterId`. Kullanıcııkça istemedikçe bu
> kolonlar çıkarılmaz. > 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 → **Dinamik servis yaşam döngüsü:** `DynamicServiceCompiler` kodu derler →
`DynamicAssemblyRegistrationService` assembly'yi tenant bağlamıyla kaydeder → `DynamicAssemblyRegistrationService` assembly'yi tenant bağlamıyla kaydeder →
`ActionDescriptorChangeProvider` MVC'ye route tablosunun değiştiğini bildirir → `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.Orders.*`, `App.BlogManagement.*`, `App.IdentityManagement.*`, `App.Reports.*`,
`App.Administration`, `App.Setting`, `App.Setup.Migrate`. `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. > **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 - Tailwind tabanlı tasarım sistemi (`components/ui`) + DevExtreme temaları; açık/koyu mod ve
tema yapılandırıcı (`ThemeConfigurator`). 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. - Dil metinleri veritabanından gelir (`Sozsoft.Languages`); dil seçici header'dadır.
Tenant bazlı çeviri geçersiz kılma desteklenir. Tenant bazlı çeviri geçersiz kılma desteklenir.
- Layout seçenekleri: dikey menü, yatay menü, yığılmış yan menü, mobil navigasyon. - 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ı 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), Ayrıntılı kurulum notları: [`configs/deployment/README.md`](configs/deployment/README.md),
[`Readme_Production.md`](configs/deployment/Readme_Production.md), [`Readme_Production.md`](configs/deployment/Readme_Production.md),
[`Readme_Devops.md`](configs/deployment/Readme_Devops.md). [`Readme_Devops.md`](configs/deployment/Readme_Devops.md).
@ -867,25 +1122,61 @@ dotnet build
dotnet run --project src/Sozsoft.Platform.HttpApi.Host dotnet run --project src/Sozsoft.Platform.HttpApi.Host
dotnet format --include .\modules\Sozsoft.Notifications\ --folder 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) # Migration (EntityFrameworkCore projesi içinde)
dotnet ef migrations add <Ad> dotnet ef migrations add <Ad>
dotnet ef database update dotnet ef database update
dotnet ef migrations remove 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: Yeni ABP modülü eklemek için:
`abp new Sozsoft.<Modul> -t module --no-ui -m none --database-provider ef` `abp new Sozsoft.<Modul> -t module --no-ui -m none --database-provider ef`
### 21.4. Seed dosyaları ### 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` **1. Depo içi (kod ile taşınan) seed'ler** — `api/src/Sozsoft.Platform.DbMigrator/Migrations/`.
- `.../Migrations/ListFormSeeder_Administration.cs` Platformun kendi ekranları, menüleri, yetkileri ve dil metinleri buradadır; değişiklikleri
- `.../Migrations/MenusData.json` derlenip yayınlanır. Yeni bir modülün nasıl kurulacağını öğrenmek için başvurulacak dosyalar:
- `.../Migrations/PermissionsData.json`
- `.../Migrations/HostData.json` - `ListFormSeeder_Saas.cs`, `ListFormSeeder_Administration.cs` — ListForm + alan tanımları
- `.../Migrations/LanguagesData.json` - `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. - `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 - Depodaki `appsettings*.json` ve deployment dokümanları geliştirme değerleri/örnek kimlik
bilgileri içerir; üretim öncesi tümü değiştirilmelidir. 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; - SQL View Designer tek yönlüdür (model → T-SQL). Elle yazılmış ya da tasarımcının kanonik
kod tabanı **.NET 10 + ABP 10** üzerindedir. Kural bölümleri geçerliliğini korur. ş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ı - 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. 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 7. Tenant isolation notes
8. Validation and test checklist 8. Validation and test checklist
9. Rollback strategy 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[] __WB_MANIFEST: PrecacheEntry[]
location: Location location: Location
navigator?: { connection?: { saveData?: boolean; effectiveType?: string } } navigator?: { connection?: { saveData?: boolean; effectiveType?: string } }
registration: { scope: string } registration: { scope: string; installing?: unknown; waiting?: unknown }
skipWaiting: () => Promise<void> skipWaiting: () => Promise<void>
clients: { clients: {
claim: () => Promise<void> claim: () => Promise<void>
@ -133,6 +133,16 @@ async function fetchWarmupList(): Promise<WarmupEntry[]> {
let warmupPromise: Promise<WarmupEntry[]> | undefined let warmupPromise: Promise<WarmupEntry[]> | undefined
const loadWarmupList = () => (warmupPromise ??= fetchWarmupList()) 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. */ /** Veri tasarrufu açıkken ya da 2G'de ~20 MB'lık ısıtma yapılmaz. */
function isWarmupAllowed() { function isWarmupAllowed() {
const connection = self.navigator?.connection 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) => { const run = async (pool: DownloadJob[], concurrency: number, deadline?: number) => {
let cursor = 0 let cursor = 0
const worker = async () => { const worker = async () => {
for (;;) { for (;;) {
const next = cursor++ const next = cursor++
if (next >= pool.length) return 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 (deadline && Date.now() > deadline) return
if (abandoned()) return
const job = pool[next] const job = pool[next]
const file = files[job.index] const file = files[job.index]
@ -453,7 +469,8 @@ async function installAssets() {
let warmupRunning = false let warmupRunning = false
async function ensureWarmup() { 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 warmupRunning = true
try { try {
const assetCache = await caches.open(ASSET_CACHE) 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 { useNavigationIcons } from '@/proxy/menus/navigation-icon.config'
import { usePermission } from '@/utils/hooks/usePermission' import { usePermission } from '@/utils/hooks/usePermission'
import { WIZARD_PERMISSION } from '@/constants/permission.constant' import { WIZARD_PERMISSION } from '@/constants/permission.constant'
import { ACTION_BUTTON_CLASS, ACTION_BUTTON_HOVER } from '@/constants/actionButton.constant'
import WizardImportDialog from './WizardImportDialog' import WizardImportDialog from './WizardImportDialog'
/** Layout tercihi diger listelerle ayni yerde (admin.lists.states) saklanir. */ /** Layout tercihi diger listelerle ayni yerde (admin.lists.states) saklanir. */
@ -178,55 +179,55 @@ const WizardFileManager = () => {
{canUpdate && ( {canUpdate && (
<Button <Button
type="button" type="button"
variant="solid" variant="plain"
color="blue-600" shape="none"
title={translate('::App.Platform.Edit')} title={translate('::App.Platform.Edit')}
icon={<FaEdit className="h-4 w-4" />}
className={`${ACTION_BUTTON_CLASS} ${ACTION_BUTTON_HOVER.blue}`}
onClick={() => onClick={() =>
navigate(ROUTES_ENUM.protected.saas.listFormManagement.wizard, { navigate(ROUTES_ENUM.protected.saas.listFormManagement.wizard, {
state: { editFileName: f.fileName }, 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. */} {/* Menünün açtığı adres yeni sekmede açılır; adres yoksa buton anlamsızdır. */}
{f.menuUrl && ( {f.menuUrl && (
<Button <Button
type="button" type="button"
variant="solid" variant="plain"
color="gray-600" shape="none"
title={translate('::App.Platform.OpenUrl')} 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')} onClick={() => window.open(f.menuUrl, '_blank', 'noopener,noreferrer')}
> />
<FaExternalLinkAlt className="w-4 h-4" />
</Button>
)} )}
{canExport && ( {canExport && (
<Button <Button
type="button" type="button"
variant="solid" variant="plain"
color="green-600" shape="none"
title={translate('::App.ListForm.WizardFileExport')} title={translate('::App.ListForm.WizardFileExport')}
loading={exportingFile === f.fileName} loading={exportingFile === f.fileName}
icon={<FaFileExport className="h-4 w-4" />}
className={`${ACTION_BUTTON_CLASS} ${ACTION_BUTTON_HOVER.green}`}
onClick={() => handleExport(f)} onClick={() => handleExport(f)}
> />
<FaFileExport className="w-4 h-4" />
</Button>
)} )}
{canDelete && ( {canDelete && (
<Button <Button
type="button" type="button"
variant="solid" variant="plain"
color="red-600" shape="none"
title={translate('::App.Platform.Delete')} title={translate('::App.Platform.Delete')}
loading={deletingFile === f.fileName} loading={deletingFile === f.fileName}
icon={<FaTrash className="h-4 w-4" />}
className={`${ACTION_BUTTON_CLASS} ${ACTION_BUTTON_HOVER.red}`}
onClick={() => onClick={() =>
setConfirm({ fileName: f.fileName, wizardName: f.wizardName || f.fileName }) setConfirm({ fileName: f.fileName, wizardName: f.wizardName || f.fileName })
} }
> />
<FaTrash className="w-4 h-4" />
</Button>
)} )}
</div> </div>
) )

View file

@ -31,6 +31,7 @@ import Select from '@/components/ui/Select'
import type { CustomComponent } from '@/proxy/developerKit/models' import type { CustomComponent } from '@/proxy/developerKit/models'
import { useStoreActions, useStoreState } from '@/store/store' import { useStoreActions, useStoreState } from '@/store/store'
import type { ListViewLayoutType } from '../admin/listForm/edit/types' 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. */ /** Layout tercihi diger listelerle ayni yerde (admin.lists.states) saklanir. */
const VIEW_STATE_CODE = 'developerkit-components' const VIEW_STATE_CODE = 'developerkit-components'
@ -146,53 +147,53 @@ const ComponentManager: React.FC = () => {
{canUpdate && ( {canUpdate && (
<Button <Button
type="button" type="button"
variant="solid" variant="plain"
color="blue-600" shape="none"
title={translate('::App.Platform.Edit')} title={translate('::App.Platform.Edit')}
icon={<FaRegEdit className="h-4 w-4" />}
className={`${ACTION_BUTTON_CLASS} ${ACTION_BUTTON_HOVER.blue}`}
onClick={() => onClick={() =>
window.open( window.open(
ROUTES_ENUM.protected.saas.developerKit.componentsEdit.replace(':id', component.id), ROUTES_ENUM.protected.saas.developerKit.componentsEdit.replace(':id', component.id),
'_blank', '_blank',
) )
} }
> />
<FaRegEdit className="w-4 h-4" />
</Button>
)} )}
<Button <Button
type="button" type="button"
variant="solid" variant="plain"
color="gray-600" shape="none"
disabled={!component.isActive || !component.routePath?.trim()} disabled={!component.isActive || !component.routePath?.trim()}
title={translate('::App.Platform.OpenUrl')} title={translate('::App.Platform.OpenUrl')}
icon={<FaExternalLinkAlt className="h-4 w-4" />}
className={`${ACTION_BUTTON_CLASS} ${ACTION_BUTTON_HOVER.slate}`}
onClick={() => { onClick={() => {
const routePath = component.routePath.startsWith('/') const routePath = component.routePath.startsWith('/')
? component.routePath ? component.routePath
: `/${component.routePath}` : `/${component.routePath}`
window.open(routePath, '_blank') window.open(routePath, '_blank')
}} }}
> />
<FaExternalLinkAlt className="w-4 h-4" />
</Button>
<Button <Button
type="button" type="button"
variant="solid" variant="plain"
color="green-600" shape="none"
title={translate('::App.Platform.View')} title={translate('::App.Platform.View')}
icon={<FaCog className="h-4 w-4" />}
className={`${ACTION_BUTTON_CLASS} ${ACTION_BUTTON_HOVER.green}`}
onClick={() => setEditorComponentId(component.id)} onClick={() => setEditorComponentId(component.id)}
> />
<FaCog className="w-4 h-4" />
</Button>
{canDelete && ( {canDelete && (
<Button <Button
type="button" type="button"
variant="solid" variant="plain"
color="red-600" shape="none"
title={translate('::App.Platform.Delete')} title={translate('::App.Platform.Delete')}
icon={<FaTrashAlt className="h-4 w-4" />}
className={`${ACTION_BUTTON_CLASS} ${ACTION_BUTTON_HOVER.red}`}
onClick={() => handleDelete(component.id)} onClick={() => handleDelete(component.id)}
> />
<FaTrashAlt className="w-4 h-4" />
</Button>
)} )}
</div> </div>
) )

View file

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

View file

@ -567,6 +567,37 @@ function hideDialogLater() {
}, NOTICE_VISIBLE_MS) }, 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) { function handleWorkerMessage(event: MessageEvent) {
const data = event.data as SwProgressMessage | undefined const data = event.data as SwProgressMessage | undefined
if (!data?.type) return if (!data?.type) return
@ -586,11 +617,20 @@ function handleWorkerMessage(event: MessageEvent) {
*/ */
const transitions = controlled && data.phase !== 'warmup' 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 * Kurulum başladığı anda ısıtma mesajları tamamen susturulur.
// birleşince satırlar tekrarlanıyor ve toplam boyut aşıyordu; geçiş sürecinde *
// ısıtma mesajları yok sayılır. * Deploy sonrası iki oturum GERÇEKTEN aynı anda koşuyor: eski aktif worker
if (data.phase === 'warmup' && (activating || state.status === 'ready')) return * `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 } 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. // Geçişli akış kapatılamaz; önceki kapatma tercihi burada geçersizleşir.
if (transitions) forceDialog() if (transitions) forceDialog()
void showUpdateDialog('downloading') void showUpdateDialog('downloading')
resetRate() activePhase = undefined
setState({ percent: 0 }) startSession(data.phase)
sessionTransitions = transitions
recalculate(toFiles(data.files), totals, { recalculate(toFiles(data.files), totals, {
status: 'downloading', status: 'downloading',
visible: dialogVisible(), visible: dialogVisible(),
reusedCount: data.reusedCount ?? 0, reusedCount: data.reusedCount ?? 0,
secondsLeft: undefined, secondsLeft: undefined,
dismissible: !transitions, dismissible: !sessionTransitions,
}) })
break break
case 'SW_PRECACHE_PROGRESS': case 'SW_PRECACHE_PROGRESS':
// Durum isteği bir yarışa takılıp cevapsız kalırsa pencere burada açılır. // Durum isteği bir yarışa takılıp cevapsız kalırsa pencere burada açılır.
if (!dialogMountRequested) void showUpdateDialog('downloading') 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, { recalculate(mergeFiles(toFiles(data.updates)), totals, {
status: 'downloading', status: 'downloading',
visible: dialogVisible(), visible: dialogVisible(),
dismissible: !transitions, dismissible: !sessionTransitions,
}) })
break break
case 'SW_PRECACHE_DONE': 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, { recalculate(keepSeq(toFiles(data.files)), totals, {
// Yeni sürüme geçilecekse 'ready'; ısıtmada iş bitti. // Yeni sürüme geçilecekse 'ready'; ısıtmada iş bitti.
status: transitions ? 'ready' : 'completed', status: sessionTransitions ? 'ready' : 'completed',
visible: transitions || dialogVisible(), visible: sessionTransitions || dialogVisible(),
percent: 100, percent: 100,
}) })
if (transitions) scheduleActivationHandoff() if (sessionTransitions) scheduleActivationHandoff()
else hideDialogLater() else hideDialogLater()
break break
case 'SW_PRECACHE_ERROR': 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. // Çalışan sürüm etkilenmedi; kullanıcıyı yeniden yükleme döngüsüne sokma.
setState({ setState({
status: 'error', status: 'error',
@ -757,8 +810,9 @@ function requestPrecacheState(reg: ServiceWorkerRegistration) {
* Worker eksik bir şey yoksa hiçbir mesaj yollamaz, pencere de ılmaz. * Worker eksik bir şey yoksa hiçbir mesaj yollamaz, pencere de ılmaz.
*/ */
function requestWarmup(reg: ServiceWorkerRegistration) { function requestWarmup(reg: ServiceWorkerRegistration) {
// Kurulum sürüyorsa zaten indiriliyor; ikinci bir oturum başlatma. // Kurulum sürüyor ya da geçişi bekliyorsa ikinci bir oturum başlatma: iki
if (reg.installing) return // 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 const worker = reg.active ?? navigator.serviceWorker.controller
worker?.postMessage({ type: 'ENSURE_WARMUP' }) worker?.postMessage({ type: 'ENSURE_WARMUP' })
} }