claude instructions dosyaları düzenlendi
This commit is contained in:
parent
422e32489f
commit
9981d5b19a
12 changed files with 3104 additions and 288 deletions
184
.github/instructions/ai.instructions.md
vendored
184
.github/instructions/ai.instructions.md
vendored
|
|
@ -14,6 +14,16 @@ Primary principle:
|
|||
|
||||
- Configuration first, code last.
|
||||
|
||||
Companion documents (this file wins on conflict):
|
||||
|
||||
| Document | Scope |
|
||||
| --- | --- |
|
||||
| `lowcode.instructions.md` | **Authoring reference**: exact schemas, enum values and worked examples for producing wizard screens, editor scripts, custom components, SQL objects and endpoints. Read it before writing any artifact. |
|
||||
| `dotnet.instructions.md` | Binding code standard for everything under `api/` |
|
||||
| `list.instructions.md` | Step-by-step procedure for adding a module / list through seeders |
|
||||
| `../../README.md` | Technical map: what exists, where it lives, how it is wired |
|
||||
| `../../CLAUDE.md` | Operating rules and standing defaults for AI agents in this repo |
|
||||
|
||||
---
|
||||
|
||||
## 2. Platform Scope
|
||||
|
|
@ -116,7 +126,7 @@ Capabilities:
|
|||
- Query
|
||||
- WebService
|
||||
- Cascading lookup parent-child behavior
|
||||
- Dynamic validation, editor options, editor scripts
|
||||
- Dynamic validation, editor options, editor scripts (see 7.1)
|
||||
- Conditional formatting and style injection
|
||||
- Grid state save/load/reset
|
||||
- User filter save/apply/delete flows
|
||||
|
|
@ -146,6 +156,34 @@ Supported editor types:
|
|||
- dxTextArea
|
||||
- dxTextBox
|
||||
|
||||
### 7.1 Editor Script and Editor Options (builder-backed)
|
||||
|
||||
`EditorScript` and `EditorOptions` are produced from a shared vocabulary, in two places that
|
||||
must stay identical:
|
||||
|
||||
| Surface | Location |
|
||||
| --- | --- |
|
||||
| Visual builder (UI) | `ui/src/views/admin/listForm/edit/json-row-operations/editor-script/scriptRecipes.ts` and `.../editor-options/optionSpecs.ts` + `presets.ts` |
|
||||
| Seeder API (C#) | `api/src/Sozsoft.Platform.Domain.Shared/Editors/` — `EditorScript`, `EditorScriptRule`, `EditorScriptCondition`, `EditorOptions`, `EditorOptionsBuilder` |
|
||||
|
||||
Rules AI must follow:
|
||||
|
||||
- Prefer a recipe over hand-written JS. Recipes cover arithmetic (`Multiply`, `Subtract`,
|
||||
`Percent`, `Sum`, `Formula`), dates (`Today`, `Days`, `Hours`), data movement (`Copy`,
|
||||
`SetValue`, `Clear`, `ApiToField`), and view/notify actions (`ReadOnly`, `Notify`, `Ask`,
|
||||
`OpenUrl`). Free-form JS is `Custom` and is the last option.
|
||||
- Generated script carries a `// @builder {...}` header on the first line that stores the rules,
|
||||
plus `// @runOnOpen` when any rule runs on form open. **Never edit the header by hand and never
|
||||
emit a script that does not match what the builder would emit** — the dialog then treats the
|
||||
script as manually edited and closes the rule editor.
|
||||
- Rule triggers: `change` (default), `open` (default-value generation), `both`. Conditions join
|
||||
with `and` / `or`.
|
||||
- When a recipe changes on one side, change its port on the other side in the same task.
|
||||
- In seeders write `EditorOptions = EditorOptions.Number(2).ShowClearButton()` style expressions
|
||||
instead of raw JSON strings. Options marked `platform: true` in `optionSpecs.ts` deserialize
|
||||
into typed DTOs (`GridBoxOptionsDto`, `TagBoxOptionsDto`, `ImageUploadOptionsDto`); wrong types
|
||||
are silently ignored.
|
||||
|
||||
---
|
||||
|
||||
## 8. Dynamic Form System
|
||||
|
|
@ -172,6 +210,8 @@ Supported component families:
|
|||
- TreeList / Tree view
|
||||
- SubForm tabs (List, Tree, Gantt, Scheduler, Form, Chart)
|
||||
- Widget Group (dashboard KPI cards)
|
||||
- TodoBoard — a Kanban board: columns come from `statusExpr` values, cards change status by drag
|
||||
and drop, new columns (statuses) can be added from the board
|
||||
|
||||
Runtime UI capabilities:
|
||||
|
||||
|
|
@ -212,6 +252,47 @@ If user requests custom code explicitly:
|
|||
- Provide a warning that low-code path is preferred.
|
||||
- Offer configuration-first alternative first.
|
||||
|
||||
### 10.1 Custom Component path (still not hand-written React)
|
||||
|
||||
When a screen genuinely cannot be expressed as a ListForm, the next step is **not** a physical
|
||||
React page. It is a Custom Component built in the Visual Designer and attached to a menu through
|
||||
the Wizard's `Custom` path.
|
||||
|
||||
- The component is stored in the database (`Name`, `RoutePath`, `Code`, `Props`, `Dependencies`,
|
||||
`DataSources`) and compiled in the browser with `@babel/standalone`.
|
||||
- The designer document lives in `Props.visualDesigner` **and** in the
|
||||
`/*__SOZSOFT_VISUAL_DESIGNER__…__*/` header of the generated code. `sourceMode` is `visual` or
|
||||
`code`; switching to `code` is one-way — the canvas cannot be restored afterwards, so do not
|
||||
propose it casually.
|
||||
- Toolbox families: `layout` (`PageContainer`, `FlexRow`, `Spacer`), `data` (`Form` — owns the
|
||||
four CRUD endpoints and acts as a container), `platform` (`ListView`, `DataGridView`,
|
||||
`TreeView`, `GanttView`, `TodoBoard`, `CardView`, `SchedulerView`, `PivotView`, `ChartView` —
|
||||
each embeds a ListForm screen through `listFormCode`), `html`/`ui`, and other custom
|
||||
components as `custom`.
|
||||
- **Reuse before drawing:** if the requirement is a list/tree/chart that a ListForm already
|
||||
serves, drop the matching `platform` node with its `listFormCode` instead of rebuilding it out
|
||||
of primitives.
|
||||
- Data binding goes through CRUD endpoints. Filter operators map to the CrudEndpoint `GetList`
|
||||
contract: `eq` is a bare query parameter, every other operator carries its name as a suffix
|
||||
(`?Name.contains=…`).
|
||||
- Permissions are two-layered and mandatory: node-level `designerPermission` for visibility, and
|
||||
per-command Form permissions (`selectPermission`, `insertPermission`, `updatePermission`,
|
||||
`deletePermission`). In `Otomatik` mode the base is the read permission the Wizard created for
|
||||
the component and the commands get `''` / `.Create` / `.Update` / `.Delete` suffixes.
|
||||
- Manager screen permissions: `App.DeveloperKit.Components{,.Create,.Update,.Delete}`.
|
||||
- Every save/delete writes `configs/seeds/{scope}/custom/{Name}.json`; the endpoints it uses are
|
||||
referenced through `crud/{Entity}.json`. A component that is not seed-backed is not done.
|
||||
|
||||
### 10.2 SQL View Designer
|
||||
|
||||
For shaping data before a screen binds to it, prefer a designed view over an inline query:
|
||||
`SqlViewDesignerDialog` produces T-SQL from a diagram (sources, joins, `CROSS/OUTER APPLY`,
|
||||
derived subqueries) plus an SSMS-style criteria grid (Column / Alias / Output / Group By / Sort /
|
||||
Filter / Or…). It is **one-way** — model to T-SQL. A view that does not parse back into the
|
||||
canonical shape drops the dialog to raw SQL mode, so hand-editing a designed view costs the
|
||||
designer. Aggregations available in the Group By column: `GroupBy`, `Where`, `SUM`, `COUNT`,
|
||||
`COUNT_DISTINCT`, `AVG`, `MIN`, `MAX`.
|
||||
|
||||
---
|
||||
|
||||
## 11. Integrations
|
||||
|
|
@ -540,13 +621,32 @@ When in doubt, AI must choose the path that preserves:
|
|||
|
||||
## 31. Seeder-Driven Low-Code Development Guide (Authoritative)
|
||||
|
||||
AI must learn and teach implementation flow primarily from these seed assets:
|
||||
There are **two seed surfaces**. AI must not mix them.
|
||||
|
||||
- api/src/Sozsoft.Platform.DbMigrator/Migrations/ListFormSeeder_Saas.cs
|
||||
- api/src/Sozsoft.Platform.DbMigrator/Migrations/MenusData.json
|
||||
- api/src/Sozsoft.Platform.DbMigrator/Migrations/PermissionsData.json
|
||||
- api/src/Sozsoft.Platform.DbMigrator/Migrations/HostData.json
|
||||
- api/src/Sozsoft.Platform.DbMigrator/Migrations/LanguagesData.json
|
||||
**A. Repository seeds (shipped with code)** — `api/src/Sozsoft.Platform.DbMigrator/Migrations/`.
|
||||
Platform-owned screens, menus, permissions and language texts. Changing them requires a build and
|
||||
a deploy. AI must learn and teach implementation flow primarily from these assets:
|
||||
|
||||
- ListFormSeeder_Saas.cs / ListFormSeeder_Administration.cs / ListFormSeeder_{Modul}.cs
|
||||
- MenusData.json
|
||||
- PermissionsData.json
|
||||
- HostData.json
|
||||
- LanguagesData.json
|
||||
- WizardDataSeeder.cs / CustomComponentDataSeeder.cs / CrudDataSeeder.cs — the seeders that read
|
||||
surface B below
|
||||
|
||||
**B. Runtime seeds (written by the running application)** — `configs/seeds/`, resolved through
|
||||
`SeedPathResolver`. The Wizard, Component Manager and CRUD Endpoint Manager write here; the
|
||||
seeders above restore them when the database is recreated. Scope folders mirror the CDN layout:
|
||||
|
||||
```
|
||||
configs/seeds/host/{wizard,custom,crud,sql/{object,execute},postgres/{object,execute}}
|
||||
configs/seeds/tenants/{tenantId}/… same layout, tenant scope
|
||||
configs/seeds/.imports/{importId}/… wizard import staging + backup (not scanned)
|
||||
```
|
||||
|
||||
Rule: anything produced at runtime must land in surface B. **A feature that disappears when the
|
||||
database is recreated is not delivered.**
|
||||
|
||||
If user asks "how to add a new module/screen", AI must answer with this exact operational sequence.
|
||||
|
||||
|
|
@ -659,6 +759,27 @@ When user asks for a screen/module, AI must answer in this order:
|
|||
|
||||
AI should produce practical, copy-adaptable artifact definitions and avoid abstract-only explanations.
|
||||
|
||||
### 31.9 Screen brief and image-based requests
|
||||
|
||||
A request arrives either as prose or as a **screenshot / mockup / photo** of the desired screen.
|
||||
Both enter the same pipeline:
|
||||
|
||||
1. Turn it into a single screen brief — purpose, data source, columns and editors, layouts,
|
||||
actions, sub-screens, widgets, approval flow, menu, permissions, tenancy, language texts.
|
||||
For an image, read it first: page regions → main region type → toolbar → columns → filters →
|
||||
form controls → row actions.
|
||||
2. Pick the tool: grid-shaped work → SQL Query Manager + Wizard; free layout / dashboard /
|
||||
single-record custom form → Custom Component. **When in doubt, pick the Wizard.**
|
||||
3. Produce the seed files in dependency order.
|
||||
|
||||
An image is a reference, not a pixel contract: build the closest thing the platform's own
|
||||
components give, and state at the end what had no equivalent. Never block on missing detail —
|
||||
state the assumption and continue; ask only where a wrong guess loses data or picks the wrong
|
||||
approver.
|
||||
|
||||
The concrete brief template, the image-element → artifact mapping and the visual-control →
|
||||
`EditorType` mapping are in `lowcode.instructions.md` §0.6–§0.7.
|
||||
|
||||
---
|
||||
|
||||
## 32. Mandatory Default Behaviors (Do Not Ask Repeatedly)
|
||||
|
|
@ -701,7 +822,54 @@ Rules:
|
|||
- Permission contract must be created/bound for both parent and child menu items.
|
||||
- Route/menu consistency remains mandatory (`Url` and screen contract must match).
|
||||
|
||||
### 32.3 AI enforcement requirement
|
||||
### 32.3 Wizard component kind and menu creation
|
||||
|
||||
`ListFormWizardDto.ComponentKind` is the wizard's fork and the first field in the seed file.
|
||||
Default is `List`, so older seed files that lack it keep working.
|
||||
|
||||
| Kind | Produces | Menu URL | Child permissions |
|
||||
| --- | --- | --- | --- |
|
||||
| `List` | ListForm + ListFormFields + Route + Menu + Permissions + language keys | `/admin/list/{ListFormCode}` | `.Create`, `.Update`, `.Delete`, `.Export`, `.Import`, `.Note` |
|
||||
| `Custom` | Menu + Permissions + language keys; the screen is the selected Custom Component | the component's `RoutePath` | `.Create`, `.Update`, `.Delete` only |
|
||||
|
||||
`Export` / `Import` / `Note` exist only in the ListForm ecosystem. Never propose them on the
|
||||
`Custom` path.
|
||||
|
||||
`CreateMenu = false` (menu-less wizard): no menu record — and no parent menu record — is created;
|
||||
ListForm, permissions and language keys are still produced. Use it when the screen is meant to be
|
||||
embedded elsewhere as a SubGrid/part. Default is `true`.
|
||||
|
||||
### 32.4 Permission group selection
|
||||
|
||||
The wizard's permission group comes from a select list backed by `GetWizardPermissionGroups`.
|
||||
Display names are read from the language key whose name equals the group name.
|
||||
|
||||
- Selecting an existing group fills `PermissionGroupDisplayNameEn/Tr`; if left blank the server
|
||||
writes the database values back, so the seed file always carries both languages.
|
||||
- Defining a new group writes those texts to the language key with the same name as the group.
|
||||
- AI must never propose a permission group without EN + TR display names.
|
||||
|
||||
### 32.5 Wizard export/import is a package, not a file
|
||||
|
||||
Exporting a wizard produces a zip whose paths are relative to the scope folder
|
||||
(`host` / `tenants/{tenantId}`) and which carries every dependency:
|
||||
|
||||
- `wizard/{file}.json`
|
||||
- `custom/{component}.json` (with its dependencies) — `Custom` path
|
||||
- `crud/{entity}.json` — endpoints those components use
|
||||
- `{sql|postgres}/{object|execute}/{object}.sql` — the object behind `SelectCommand` on the
|
||||
`List` path
|
||||
|
||||
Import is two-phase: `AnalyzeImport` (unzip to `.imports/{id}/staged`, classify each entry as
|
||||
`New` / `Identical` / `Conflict`, conflicts resolved in a diff editor) then `ApplyImport`
|
||||
(file-by-file write, previous content backed up so `RollbackImport` can undo the whole batch),
|
||||
closed by `CompleteImport`. Limits: allowed root folders `wizard`, `crud`, `custom`, `sql`,
|
||||
`postgres` (and only `object` / `execute` under the SQL provider folders); 5 MB per entry, 50 MB
|
||||
per archive, 500 entries. Permissions: `App.Listforms.Wizard.Export` / `.Import`.
|
||||
|
||||
AI must not propose moving a screen between environments by copying a single json file.
|
||||
|
||||
### 32.6 AI enforcement requirement
|
||||
|
||||
AI must proactively apply these defaults in:
|
||||
|
||||
|
|
|
|||
4
.github/instructions/list.instructions.md
vendored
4
.github/instructions/list.instructions.md
vendored
|
|
@ -7,6 +7,10 @@ This document summarizes the rules, standards, and step-by-step instructions for
|
|||
---
|
||||
|
||||
|
||||
> **Bu doküman depo içi seeder'larla (DbMigrator/Migrations) modül eklemeyi anlatır.**
|
||||
> Çalışma zamanında Wizard / Component Manager / CRUD Endpoint Manager ile üretim yapacaksan
|
||||
> `lowcode.instructions.md` esastır; oradaki seed dosyaları `configs/seeds` altına düşer.
|
||||
|
||||
## General Principles
|
||||
- **Configuration First:** Always prefer platform configuration (menus, permissions, forms, localization) over custom code.
|
||||
- **Modularization:** Each module (e.g., {Modul}) must have its own seeder, permission group, and localization entries.
|
||||
|
|
|
|||
2064
.github/instructions/lowcode.instructions.md
vendored
Normal file
2064
.github/instructions/lowcode.instructions.md
vendored
Normal file
File diff suppressed because it is too large
Load diff
334
README.md
334
README.md
|
|
@ -68,7 +68,7 @@ React/C# kodu yazmadan, yalnızca konfigürasyon ile mümkündür.
|
|||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────────┐
|
||||
│ UI — React 18 + Vite + TypeScript + DevExtreme + Tailwind (PWA) │
|
||||
│ UI — React 19 + Vite + TypeScript + DevExtreme + Tailwind (PWA) │
|
||||
│ ├─ DynamicRouter (route kayıtlarından React Router üretimi) │
|
||||
│ ├─ ListForm runtime (Grid / Pivot / Tree / Chart / Gantt / ...) │
|
||||
│ ├─ Form runtime (DevExtreme Form, metadata ile alan üretimi)│
|
||||
|
|
@ -128,10 +128,10 @@ DevExtreme bileşenleri bu metadata'ya göre kendini kurar.
|
|||
|
||||
| Bileşen | Sürüm / Notlar |
|
||||
| --- | --- |
|
||||
| React | 18.3 |
|
||||
| React | 19.2 |
|
||||
| Build | Vite 8 + TypeScript 5.9 |
|
||||
| UI kütüphaneleri | DevExtreme 25.2 (React), Tailwind CSS 3.4, kendi `components/ui` tasarım sistemi |
|
||||
| Routing | react-router-dom 6 (dinamik route üretimi) |
|
||||
| UI kütüphaneleri | DevExtreme 26.1 (React), Tailwind CSS 3.4, kendi `components/ui` tasarım sistemi |
|
||||
| Routing | react-router-dom 7 (dinamik route üretimi) |
|
||||
| State | easy-peasy (global store) + @tanstack/react-query |
|
||||
| Form | Formik + Yup, DevExtreme Form |
|
||||
| Kod editörü | Monaco Editor |
|
||||
|
|
@ -183,7 +183,10 @@ sozsoft-platform/
|
|||
│ │ # tenants/{tenantId}/{crud,custom,wizard,sql,postgres}
|
||||
│ │ # {sql|postgres}/{object,execute}
|
||||
│ └── ai/ # AI entegrasyonu (n8n akış tanımları)
|
||||
├── .github/instructions/ # ai.instructions.md (bağlayıcı platform kuralları), list.instructions.md
|
||||
├── .github/instructions/ # ai.instructions.md (bağlayıcı platform kuralları)
|
||||
│ # lowcode.instructions.md (artefakt üretim referansı: şemalar, örnekler)
|
||||
│ # dotnet.instructions.md (api/ için bağlayıcı .NET/ABP kod standardı)
|
||||
│ # list.instructions.md (modül/liste ekleme prosedürü)
|
||||
├── CLAUDE.md # Claude Code için operasyon kuralları
|
||||
└── README.md
|
||||
```
|
||||
|
|
@ -383,11 +386,17 @@ Ekran davranışı büyük ölçüde JSON kolonlarında saklanır. Öne çıkanl
|
|||
|
||||
Aynı `ListForm` tanımı, aynı veri hattı üzerinden birden fazla görünümle sunulabilir:
|
||||
|
||||
`Grid` · `Pivot` · `Tree` · `Chart` · `Gantt` · `Scheduler` · `CardView` · `TodoBoard` · `Widget Group`
|
||||
`Grid` · `Pivot` · `Tree` · `Chart` · `Gantt` · `Scheduler` · `CardView` · `TodoBoard (Kanban)` ·
|
||||
`Widget Group`
|
||||
|
||||
Kullanıcı görünümler arasında geçiş yapabilir; grid durumu (sütun sırası, genişlik, filtre,
|
||||
gruplama) kullanıcı bazlı saklanır ve sıfırlanabilir.
|
||||
|
||||
**TodoBoard artık bir Kanban panosudur:** kolonlar `TodoOptionJson` içindeki `statusExpr`
|
||||
değerlerinden üretilir, kartlar kolonlar arasında sürüklenerek durum değiştirir ve panodan yeni
|
||||
kolon (durum) eklenebilir. Kart içeriği `titleExpr`, `descriptionExpr`, `dueDateExpr`, `tagExpr`
|
||||
ve atanan kullanıcı alanlarından kurulur.
|
||||
|
||||
### 7.4. Alan (field) davranışı
|
||||
|
||||
- **Editör tipleri:** `dxTextBox`, `dxTextArea`, `dxNumberBox`, `dxDateBox`, `dxDateRangeBox`,
|
||||
|
|
@ -398,9 +407,67 @@ gruplama) kullanıcı bazlı saklanır ve sıfırlanabilir.
|
|||
- **Doğrulama:** `ValidationRuleJson` ile required/range/pattern/custom kuralları.
|
||||
- **Editör script:** `EditorScript` alanı ile alan değişiminde çalışan JS (örn. tarih farkından
|
||||
gün hesaplama, miktar × birim fiyat toplamı). Çalıştırma `utils/editorScriptRuntime.ts` üzerinden yapılır.
|
||||
- **Editör seçenekleri:** `EditorOptions` alanı DevExtreme editörüne geçirilen JSON'dur
|
||||
(format, maske, placeholder, yükseklik, buton görünürlüğü, platform editörlerinin tipli
|
||||
ayarları). Elle JSON yazmak zorunlu değildir; bkz. 7.4.1.
|
||||
- **Koşullu biçimlendirme:** `ColumnStylingJson`, `ColumnCssClass`/`ColumnCssValue`.
|
||||
- **Alan bazlı yetki:** `PermissionJson` ile sütun düzeyinde okuma/yazma/dışa aktarma kontrolü.
|
||||
|
||||
#### 7.4.1. Script Builder ve Editor Options Builder
|
||||
|
||||
`EditorScript` ve `EditorOptions` alanları hem görsel olarak hem de seeder kodundan üretilir;
|
||||
**iki tarafın çıktısı birebir aynı olmak zorundadır.**
|
||||
|
||||
**Editor Script Builder** (`json-row-operations/editor-script/`)
|
||||
|
||||
- Kural sözlüğü `scriptRecipes.ts` içindedir; her tarif (recipe) tek satırlık runtime çağrısı üretir.
|
||||
Gruplar: hesap (`calc`), veri (`data`), görünüm (`view`), bildirim, API.
|
||||
- Üretilen script'in ilk satırındaki `// @builder {...}` başlığı kuralları saklar. Dialog script'i
|
||||
regex ile çözmeye çalışmaz, bu başlıktan geri okur; başlık bozulursa/elle düzenlenirse script
|
||||
"manuel" sayılır ve kural editörü kapanır.
|
||||
- Kural tetikleyicileri: `change` (varsayılan), `open` (form açılışında — varsayılan değer üretmek
|
||||
için, script başına `// @runOnOpen` direktifi eklenir), `both`.
|
||||
- Koşullar `and`/`or` ile birleşir: `equals`, `notEquals`, `contains`, `empty`, `notEmpty`,
|
||||
`greaterThan`, `lessThan`, `isTrue`, `isFalse`, `always`.
|
||||
- Dialog'un kendisi ortaktır (`components/scriptBuilder/ScriptBuilderDialog.tsx` + IntelliSense);
|
||||
ListForm lehçesi `formScriptDialect.ts`, Visual Designer lehçesi `designerScriptDialect.ts`
|
||||
ile aynı sözleşmeye bağlanır.
|
||||
|
||||
**Seeder tarafı (C#)** — `Sozsoft.Platform.Domain.Shared/Editors/`
|
||||
|
||||
`scriptRecipes.ts` ve editorOptions sözlüğünün C# portudur. Seeder ile basılan script/JSON,
|
||||
dialogda açılıp görsel olarak düzenlenebilir kalır. Bir tarif TypeScript tarafında değişirse
|
||||
buradaki karşılığı da güncellenmelidir.
|
||||
|
||||
| Tip | Kullanım |
|
||||
| --- | --- |
|
||||
| `EditorScript` | `Multiply`, `Subtract`, `Percent`, `Sum`, `Formula`, `Today`, `Days`, `Hours`, `Copy`, `SetValue`, `Clear`, `ReadOnly`, `Notify`, `Ask`, `OpenUrl`, `ApiToField`, `Custom` → `EditorScript.Build(rules)` |
|
||||
| `EditorScriptRule` | `.When(...)` / `.WhenAny(...)` koşulları, `.OnChange()` / `.OnOpen()` / `.OnOpenAndChange()` tetikleyicisi; `string`'e örtük dönüşür |
|
||||
| `EditorScriptCondition` | `Is`, `IsNot`, `Contains`, `IsEmpty`, `IsNotEmpty`, `GreaterThan`, `LessThan`, `IsTrue`, `IsFalse`, `Always` |
|
||||
| `EditorOptions` | Hazır başlangıçlar: `Disabled()`, `ReadOnly()`, `ShowClearButton()`, `Multiline(h)`, `Number(precision)`, `Percent()`, `Date()`, `DateTime()`, `Time(interval)`, `Phone()`, `Slider()`, `ImageUpload()`, `Html(h)` |
|
||||
| `EditorOptionsBuilder` | Akıcı ekleme: `.Placeholder()`, `.MaxLength()`, `.Height()`, `.Width()`, `.Format()`, `.Mask()`, `.Flag()`, `.Text()`, `.Number()`, `.Json()`; `string`'e örtük dönüşür |
|
||||
|
||||
```csharp
|
||||
EditorOptions = EditorOptions.Number(4).ShowClearButton(),
|
||||
EditorOptions = EditorOptions.Multiline(60).Placeholder("Açıklama"),
|
||||
EditorScript = EditorScript.Build(
|
||||
EditorScript.Multiply("Quantity", "UnitPrice", "Total"),
|
||||
EditorScript.Percent("Total", "VatRate", "VatAmount", mode: EditorScriptPercentMode.Add)
|
||||
.When(EditorScriptCondition.IsNotEmpty("VatRate"))),
|
||||
```
|
||||
|
||||
**Editor Options Builder** (`json-row-operations/editor-options/`)
|
||||
|
||||
- UI tamamen `optionSpecs.ts` veri sözlüğünden üretilir; yeni bir ayar eklemek için oraya tek
|
||||
satır yazmak yeterlidir. Her ayar `path` (örn. `format.precision`), tip (`boolean` üç durumlu /
|
||||
`number` / `text` / `select` / `size` / `stringList` / `json`), grup ve hangi editörlerde
|
||||
anlamlı olduğu bilgisini taşır.
|
||||
- `platform: true` işaretli ayarlar backend'in tipli DTO'ya (`GridBoxOptionsDto`,
|
||||
`TagBoxOptionsDto`, `ImageUploadOptionsDto`) deserialize ettiği alanlardır; yanlış tipte
|
||||
yazılırsa sessizce yok sayılır.
|
||||
- `presets.ts` hazır kalıplar sunar (HTML editör tam araç çubuğu, telefon maskesi, tarih/saat…);
|
||||
kalıp mevcut JSON ile **birleştirilir**, diğer ayarları silmez.
|
||||
|
||||
### 7.5. İş akışı ve onay
|
||||
|
||||
`FormTabWorkflow` ve `views/admin/listForm/workflow/WorkflowDesigner.tsx` üzerinden görsel
|
||||
|
|
@ -411,15 +478,57 @@ butonlar gösterilebilir.
|
|||
|
||||
### 7.6. Yeni ekran ekleme akışı (Wizard)
|
||||
|
||||
`/admin/listform/wizard` altındaki 11 adımlı sihirbaz, aşağıdaki yedi artefaktı tek akışta üretir:
|
||||
`/admin/listform/wizard` altındaki sihirbaz, aşağıdaki artefaktları tek akışta üretir:
|
||||
|
||||
1. **ListForm** kaydı (kod, ad, başlık, veri kaynağı, select komutu, anahtar alan)
|
||||
2. **ListFormField** kümesi (sütunlar, editörler, lookup'lar, doğrulamalar)
|
||||
3. **Route** kaydı (`key`, `path`, `componentPath`, `routeType`, `authority`)
|
||||
4. **Menu** kaydı (`ParentCode`, `Code`, `DisplayName`, `Url`, `Icon`, `RequiredPermissionName`, `Order`)
|
||||
5. **Permission** kayıtları (`.Default`, `.Create`, `.Update`, `.Delete`, `.Export`, `.Import`, `.Note`)
|
||||
6. Gerekiyorsa **ayar/entegrasyon** bağımlılıkları
|
||||
7. **Doğrulama ve geri alma** notları
|
||||
6. **Dil anahtarları** (menü, başlık, açıklama; EN + TR)
|
||||
7. Gerekiyorsa **ayar/entegrasyon** bağımlılıkları
|
||||
8. **Seed dosyası** — `configs/seeds/{host|tenants/{tenantId}}/wizard/{zaman damgası}_{Ad}.json`
|
||||
9. **Doğrulama ve geri alma** notları
|
||||
|
||||
**Adım bileşenleri.** Adımlar numaralı dosya adları yerine işlevleriyle adlandırılır ve
|
||||
`React.lazy` ile yüklenir; aynı anda yalnızca görünen adım indirilir, sihirbazın açılış maliyeti
|
||||
adım sayısından bağımsızdır.
|
||||
|
||||
| Adım | Dosya | İçerik |
|
||||
| --- | --- | --- |
|
||||
| Menü ve kimlik | `WizardStepMenu.tsx` | Wizard adı, ListForm kodu, menü ağacı/üst menü, ikon, sıra, izin grubu, dil metinleri |
|
||||
| Veri ayarları | `WizardStepDataSettings.tsx` | Veri kaynağı, select komutu tipi, anahtar alan, CRUD endpoint diyaloğu |
|
||||
| Alanlar | `WizardStepFields.tsx` | Sütun grupları, editör tipleri, lookup, doğrulama |
|
||||
| Alt formlar | `WizardStepSubForms.tsx` | Ana–detay ilişki eşlemesi |
|
||||
| Widget'lar | `WizardStepWidgets.tsx` | KPI kartları |
|
||||
| İş akışı | `WizardStepWorkflow.tsx` | Onay/koşul grafiği |
|
||||
| Düzen adımları | `WizardStepTodoLayout` · `WizardStepTreeLayout` · `WizardStepGanttLayout` · `WizardStepSchedulerLayout` | Yalnızca ilgili görünüm açıksa görünür |
|
||||
| Bileşen | `WizardStepComponent.tsx` · `WizardStepCustomComponent.tsx` | Custom yolunda bağlanacak bileşen |
|
||||
| Yayınlama | `WizardStepDeploy.tsx` | Özet, doğrulama ve seed dosyasının üretimi |
|
||||
|
||||
Menü ağacı yardımcıları (`menuTree.ts`) adım bileşenlerinden ayrı bir modüldedir; böylece Wizard
|
||||
ve `SqlTableDesignerDialog` bu fonksiyonları kullanırken adım bileşenlerini pakete çekmez.
|
||||
|
||||
**İki yol: `ComponentKind`.** `ListFormWizardDto.ComponentKind` sihirbazın yol ayrımıdır ve seed
|
||||
dosyasında ilk alandır. Varsayılan `List` olduğu için bu alanı taşımayan eski seed dosyaları
|
||||
eskisi gibi çalışır.
|
||||
|
||||
| `ComponentKind` | Üretilen | Menü URL'i | Yetkiler |
|
||||
| --- | --- | --- | --- |
|
||||
| `List` | ListForm + ListFormField + Route + Menu + Permission + dil anahtarları | `/admin/list/{ListFormCode}` | `.Create`, `.Update`, `.Delete`, `.Export`, `.Import`, `.Note` |
|
||||
| `Custom` | Menü + Permission + dil anahtarları; ekran seçilen Custom Component'tir | Component'in `RoutePath` değeri | Yalnızca `.Create`, `.Update`, `.Delete` (Export/Import/Note ListForm'a özgüdür) |
|
||||
|
||||
**Menüsüz wizard (`CreateMenu = false`).** Menü (ve gerekiyorsa üst menü) kaydı hiç üretilmez;
|
||||
ListForm, permission ve dil anahtarları her iki durumda da üretilir. Menüsüz wizard'lar başka bir
|
||||
ekranda SubGrid/parça olarak kullanılmak üzere tanımlanır. Alan varsayılanı `true` olduğundan bu
|
||||
alanı taşımayan eski seed dosyaları etkilenmez.
|
||||
|
||||
**İzin grubu seçimi.** Menü adımında izin grubu bir seçim listesinden gelir
|
||||
(`GetWizardPermissionGroups`). Görünen adlar, grup adıyla aynı olan dil anahtarının metinlerinden
|
||||
okunur; seçim yapıldığında `PermissionGroupDisplayNameEn/Tr` forma doldurulur. Yeni bir grup
|
||||
tanımlanırsa grup adıyla aynı dil anahtarına bu metinler yazılır; mevcut grup seçilip alanlar boş
|
||||
bırakılırsa sunucu veritabanındaki değeri geri yazar. Böylece seed dosyası her durumda iki dilli
|
||||
görünen adı taşır.
|
||||
|
||||
Dinamik liste ekranlarının route deseni `/admin/list/{ListFormCode}` biçimindedir; form
|
||||
görünümleri `/admin/form/{listFormCode}/:id`, grafik `/admin/chart/{listFormCode}`, pivot
|
||||
|
|
@ -429,13 +538,49 @@ Var olan bir ekranı düzenlemek için `/admin/listform/edit/{listFormCode}` —
|
|||
yapılandırması sekmeli bir editörde yönetilir (Veritabanı, Sütunlar, Filtreler, Düzenleme,
|
||||
Yetkiler, Alt Form, Pivot, Tree, Gantt, Scheduler, Todo, Widget, Workflow, Durum, Grafik sekmeleri).
|
||||
|
||||
**Wizard File Manager** (`/admin/listform/wizard` liste ekranı) üretilmiş seed dosyalarını yönetir:
|
||||
liste/kart görünümü arasında geçiş (tercih diğer listelerle aynı yerde, `admin.lists.states`
|
||||
altında saklanır), arama, düzenleme (dosyanın yerine geçen yeni bir çalıştırma), silme, veritabanı
|
||||
migrate tetikleme, dışa aktarma ve içe aktarma. Butonlar `App.Listforms.Wizard.*` yetkileriyle
|
||||
gizlenir; asıl kontrol `ListFormWizardAppService` üzerindedir.
|
||||
|
||||
### 7.7. İçe/dışa aktarma
|
||||
|
||||
**Veri (ekran içi)**
|
||||
|
||||
- **Dışa aktarma:** xlsx, csv, pdf (grid durumuna ve görünür sütunlara saygı duyar).
|
||||
- **İçe aktarma:** `components/importManager` altındaki dosya yükleme → önizleme → ilerleme →
|
||||
sonuç akışı; sonuçlar `ListFormImportLog` üzerinde satır bazında raporlanır. Şablon dosyası
|
||||
ekran tanımından üretilir.
|
||||
|
||||
**Ekran tanımı (wizard seed paketi)**
|
||||
|
||||
Bir ekranın tanımı, bağımlılıklarıyla birlikte tek bir zip olarak taşınır. Zip içindeki yollar seed
|
||||
kapsam klasörü (`host` / `tenants/{tenantId}`) kökü baz alınarak yazılır; arşiv başka bir ortamda
|
||||
aynı klasör düzenine doğrudan açılabilir.
|
||||
|
||||
| Yol | İçerik |
|
||||
| --- | --- |
|
||||
| `wizard/{dosya}.json` | Wizard tanımının kendisi |
|
||||
| `custom/{component}.json` | Custom yolunda bağlı component ve bağımlılıkları |
|
||||
| `crud/{entity}.json` | Component'lerin/nesnenin kullandığı CRUD endpoint tanımları |
|
||||
| `{sql\|postgres}/{object\|execute}/{nesne}.sql` | List yolunda `SelectCommand`'a karşılık gelen SQL nesnesi |
|
||||
|
||||
**İçe aktarma iki adımlıdır** (`WizardImportDialog`):
|
||||
|
||||
1. **Analiz** (`AnalyzeImport`) — zip, seed kökü altındaki `.imports/{importId}/staged` klasörüne
|
||||
açılır (nokta ile başladığı için seeder taramalarına girmez) ve her dosya hedefteki karşılığıyla
|
||||
karşılaştırılır: `New` (hedefte yok), `Identical` (aynı, sorulmaz), `Conflict` (farklı).
|
||||
Çakışan dosyalar Monaco `DiffEditor` ile karşılaştırılıp birleştirilir.
|
||||
2. **Uygulama** (`ApplyImport`) — dosyalar tek tek yazılır, ilerleme satır satır gösterilir. Yazılan
|
||||
her dosyanın önceki hâli `.imports/{importId}/backup` altına alındığı için hata durumunda
|
||||
`RollbackImport` ile toplu işlem geri alınabilir; `CompleteImport` oturumu kapatır.
|
||||
|
||||
Güvenlik sınırları: yalnızca `wizard`, `crud`, `custom`, `sql`, `postgres` kök klasörleri
|
||||
(SQL sağlayıcı klasörlerinin altında yalnızca `object` ve `execute`) kabul edilir; dosya başına
|
||||
5 MB, arşiv başına 50 MB, en çok 500 girdi. Kabul edilmeyen girdiler analiz sonucunda uyarı
|
||||
olarak listelenir. Yetkiler: `App.Listforms.Wizard.Export` / `App.Listforms.Wizard.Import`.
|
||||
|
||||
---
|
||||
|
||||
## 8. Developer Kit
|
||||
|
|
@ -444,12 +589,40 @@ Teknik kullanıcılar için `/admin/developerkit` altında toplanan araçlar:
|
|||
|
||||
| Araç | Route | Ne işe yarar |
|
||||
| --- | --- | --- |
|
||||
| **SQL Query Manager** | `/admin/sql-manager` | Nesne gezgini, Monaco tabanlı SQL editörü, sonuç grid'i, **tablo tasarımcısı** ve **CRUD endpoint yönetimi**. |
|
||||
| **SQL Query Manager** | `/admin/sqlQueryManager` | Nesne gezgini, Monaco tabanlı SQL editörü, sonuç grid'i, **tablo tasarımcısı**, **view tasarımcısı** ve **CRUD endpoint yönetimi**. |
|
||||
| **Custom Endpoint** | `/admin/list/App.DeveloperKit.CustomEndpoints` | SQL veya servis tanımından REST endpoint üretimi; GET/POST/PUT/DELETE için ayrı yetki kapıları (`App.DeveloperKit.CustomEndpoints.*`) ve kullanıcı/rol/global erişim kuralları. |
|
||||
| **CRUD Endpoint** | SQL Query Manager içinde | Tablo tanımından tam CRUD endpoint kümesi üretimi. Ayrı ekranı yoktur; bkz. aşağıdaki not. |
|
||||
| **Dynamic Service** | `/admin/developerkit/dynamic-services` | C# kodunu tarayıcıda yazıp Roslyn ile derleme (`TestCompile`), yayınlama (`Publish`) ve çalışan uygulamaya controller olarak kaydetme. Yetkiler: Create/Edit/Delete/Manage/TestCompile/Publish/ViewCode. |
|
||||
| **Custom Component** | `/admin/developerkit/components` | React bileşenini veritabanında saklama; `@babel/standalone` ile tarayıcıda derleyip route'a bağlama. |
|
||||
| **Custom Component** | `/admin/developerkit/components` | React bileşenini veritabanında saklama; `@babel/standalone` ile tarayıcıda derleyip route'a bağlama. Liste/kart görünümü, arama; yetkiler `App.DeveloperKit.Components{,.Create,.Update,.Delete}`. |
|
||||
| **Visual Designer** | Component editörü içinde | Sürükle-bırak kanvas ile bileşen üretimi ve kod üretimi (`visualDesigner/codeGenerator.ts`). |
|
||||
| **ListForm** | `/admin/list/App.Listforms.Listform` | Var olan ekranların ham tanım listesi; buradan `/admin/listform/edit/{kod}` sekmeli editörüne geçilir. |
|
||||
| **Wizard Manager** | `/admin/listform/wizardManager` | Üretilmiş wizard seed dosyalarının listesi/kartı, düzenleme, silme, DB migrate, export/import. Yetki `App.Listforms.Wizard{,.Create,.Update,.Delete,.Export,.Import}`. |
|
||||
|
||||
> Bu ekranların artefakt şemaları, enum değerleri ve çalışan örnekleri
|
||||
> [`.github/instructions/lowcode.instructions.md`](.github/instructions/lowcode.instructions.md)
|
||||
> dosyasındadır.
|
||||
|
||||
### 8.1. SQL View Designer
|
||||
|
||||
`SqlViewDesignerDialog` (+ `sqlViewDesigner/`), SSMS Query Designer düzeninde görsel view
|
||||
oluşturma aracıdır. Üç panel:
|
||||
|
||||
- **Diagram** (`DiagramPane.tsx`) — `ViewSource` kutuları (x/y konumlu, daraltılabilir, yalnızca
|
||||
seçili kolonları gösterebilir) ve aralarındaki JOIN okları. Kaynak bir tablo/view olabileceği
|
||||
gibi `CROSS/OUTER APPLY` ya da türetilmiş bir alt sorgu da olabilir; alt sorgunun gövdesi
|
||||
olduğu gibi saklanır. JOIN türleri: `INNER`, `LEFT`, `RIGHT`, `FULL`, `CROSS`; koşul
|
||||
operatörleri `=`, `<>`, `>`, `>=`, `<`, `<=`.
|
||||
- **Criteria grid** (`CriteriaGrid.tsx`) — Column / Alias / Output / Group By / Sort / Filter /
|
||||
Or… satırları. Bir satır ya `sourceId + columnName` referansıdır ya da serbest bir SQL
|
||||
ifadesidir. Group By sütunu `GroupBy`, `Where` (satır çıktıya girmez, yalnızca filtre taşır) ve
|
||||
`SUM`/`COUNT`/`COUNT_DISTINCT`/`AVG`/`MIN`/`MAX` değerlerini alır. Filtre hücreleri SSMS'teki
|
||||
gibi serbest yüklemdir (`> 100`, `LIKE '%abc%'`, `IS NULL`).
|
||||
- **T-SQL önizleme** — model → SQL üretimi (`generateViewSql`).
|
||||
|
||||
Tasarımcı tek yönlü çalışır: **model → T-SQL**. Mevcut bir view açılırken `parseViewSql` ile geri
|
||||
okunmaya çalışılır; üretilen kanonik şekle uymayan bir tanım gelirse dialog **ham SQL moduna**
|
||||
düşer ve kullanıcı Query Editor'dan devam eder (`onOpenInEditor`). PostgreSQL bağlantılarında
|
||||
`isPostgreSql` bayrağı ile söz dizimi buna göre üretilir.
|
||||
|
||||
> **CRUD Endpoint, SQL Query Manager ile birleştirildi.** Ayrı `/admin/developerkit/endpoints`
|
||||
> ekranı ve menüsü kaldırıldı. Endpoint üretme, aktif/pasif etme, test etme ve silme işlemleri
|
||||
|
|
@ -470,6 +643,67 @@ Teknik kullanıcılar için `/admin/developerkit` altında toplanan araçlar:
|
|||
> `LastModifierId`, `IsDeleted`, `DeletionTime`, `DeleterId`. Kullanıcı açıkça istemedikçe bu
|
||||
> kolonlar çıkarılmaz.
|
||||
|
||||
### 8.2. Custom Component ve Visual Designer
|
||||
|
||||
**Saklanan tanım.** Bir custom component `Name`, `RoutePath`, `Code` (JSX), `Props`,
|
||||
`Dependencies` ve `DataSources` alanlarından oluşur. Görsel tasarımcı dokümanı iki yerde tutulur:
|
||||
`Props.visualDesigner` içinde (nodes, canvas, lifecycle, dataSources) ve üretilen kodun ilk
|
||||
satırındaki `/*__SOZSOFT_VISUAL_DESIGNER__…__*/` yorumunda. Böylece yalnızca kod elinizdeyse bile
|
||||
tasarımcı dokümanı geri okunabilir. `sourceMode` alanı `visual` ya da `code` olur; kod moduna
|
||||
geçilen bir bileşen kanvasa geri dönmez.
|
||||
|
||||
**Seed senkronizasyonu.** Component Manager üzerinden kaydedilen/silinen her bileşen
|
||||
`configs/seeds/{host|tenants/{tenantId}}/custom/{Name}.json` olarak yazılır;
|
||||
`CustomComponentDataSeeder` veritabanı silinip yeniden oluşturulduğunda aynı dosyaları okuyup geri
|
||||
yükler. Dosya düzeni bilinçli olarak `TenantData.json` içindeki `CustomComponents` bloğuyla
|
||||
aynıdır. `DataSources` sütunu tasarımcı dokümanından türetilir
|
||||
(`CustomComponentDataSourceResolver`): Data sekmesindeki her endpoint `method + path` ile CRUD
|
||||
endpoint kataloğunda aranır, eşleşenler `EntityName` + `crud/{EntityName}.json` referansını taşır,
|
||||
elle yazılmış olanlar listede kalır ama bu alanları boş gelir. Aynı çözüm hem kaydetmede (katalog
|
||||
veritabanından) hem seed'de (katalog `crud/*.json` dosyalarından) kullanıldığı için sütun iki
|
||||
yolda da aynı üretilir.
|
||||
|
||||
**Toolbox.** Kanvasa bırakılabilecekler beş aileye ayrılır:
|
||||
|
||||
| Aile | İçerik |
|
||||
| --- | --- |
|
||||
| `layout` | `PageContainer`, `FlexRow` (kolon sayısı, ilk kolon genişliği, hizalama, gap, wrap), `Spacer` |
|
||||
| `data` | `Form` — ASP.NET'in Form + FormView karşılığı; dört CRUD endpoint'ini sahiplenen kapsayıcı |
|
||||
| `platform` | `ListView`, `DataGridView`, `TreeView`, `GanttView`, `TodoBoard`, `CardView`, `SchedulerView`, `PivotView`, `ChartView` — hepsi `listFormCode` ile bir ListForm ekranını gömer |
|
||||
| `html` / `ui` | Ham HTML etiketleri ve `components/ui` tasarım sistemi bileşenleri (sözleşmeleri metadata'dan okunur) |
|
||||
| `custom` | Başka custom component'ler (bağımlılık olarak kaydedilir) |
|
||||
|
||||
**Form bileşeni.** `selectEndpoint` / `insertEndpoint` / `updateEndpoint` / `deleteEndpoint`,
|
||||
`keyFieldName`, `collectionPath`, `keySource` + `keyParamName`, `previewKeyValue`, `autoLoad`,
|
||||
`showToolbar`, `columnCount`, `gap` özellikleriyle yapılandırılır. İçine bırakılan her bileşen
|
||||
Select sonucunun bir kolonuna bağlanır ve Save/Delete üzerinden geri yazar.
|
||||
|
||||
**Veri bağlama.** `DesignerBinding` bir kaynağa (`sourceId`) ve yola (`path`) bağlanır; `labelPath`
|
||||
/ `valuePath` seçim bileşenleri içindir. `columns` ile ekranda görünmeyen ek sütunlar da taşınır
|
||||
ve script'ten `refs.<ad>.getColumn('sutun')` ile okunup başka bir bileşenin değerine ya da Form
|
||||
kaydına yazılır. Filtreler CrudEndpoint `GetList` sözleşmesine göre üretilir: `eq` çıplak query
|
||||
parametresi (`?RoleId=…`), diğerleri son ek taşır (`?Name.contains=…`). Operatörler: `eq`, `ne`,
|
||||
`contains`, `startswith`, `endswith`, `gt`, `gte`, `lt`, `lte`, `in`, `isnull`, `notnull`.
|
||||
|
||||
**Yetki modeli.** İki katmanlıdır:
|
||||
|
||||
- **Node görünürlüğü** — `designerPermission` prop'u dolu olan bir düğüm, yetki verilmemişse
|
||||
render edilmez.
|
||||
- **Form komutları** — her komut (`selectPermission`, `insertPermission`, `updatePermission`,
|
||||
`deletePermission`) ya serbesttir (boş) ya da bir yetkiye bağlıdır. `Otomatik` modda temel,
|
||||
Wizard'ın bu bileşen için ürettiği okuma yetkisidir (menü kaydının korunduğu yetkinin aynısı) ve
|
||||
komutlara `''` / `.Create` / `.Update` / `.Delete` son ekleri eklenir. `Özel` modda yetki adı
|
||||
elle yazılır.
|
||||
|
||||
**Script.** Tasarımcı, ListForm Editor Script ile aynı ortak dialog'u kullanır
|
||||
(`designerScriptDialect.ts` + `designerScriptRecipes.ts`); tarif grupları bileşen erişimi, API
|
||||
çağrıları, form ve olay/sayfa başlıkları altında toplanır.
|
||||
|
||||
**Diğer notlar.** `PlatformIcon` tasarımcı dokümanındaki ikon adını çözer; `selectComponents.ts`
|
||||
`Select.componentAs` için saklanan adı (`ReactSelect`, `CreatableSelect`, `AsyncSelect`) gerçek
|
||||
bileşene çevirir — kanvas ve üretilen bileşenin aynı adı aynı şekilde yorumlamasını sağlayan tek
|
||||
nokta budur. Yeni bileşen `configs/seeds/host/custom/NewComponent.json` şablonundan başlar.
|
||||
|
||||
**Dinamik servis yaşam döngüsü:** `DynamicServiceCompiler` kodu derler →
|
||||
`DynamicAssemblyRegistrationService` assembly'yi tenant bağlamıyla kaydeder →
|
||||
`ActionDescriptorChangeProvider` MVC'ye route tablosunun değiştiğini bildirir →
|
||||
|
|
@ -503,6 +737,19 @@ Route.Authority ──┘ └► Permission
|
|||
`App.Orders.*`, `App.BlogManagement.*`, `App.IdentityManagement.*`, `App.Reports.*`,
|
||||
`App.Administration`, `App.Setting`, `App.Setup.Migrate`.
|
||||
|
||||
Aksiyon yetkisi olan alt gruplar (UI tarafındaki karşılıkları `constants/permission.constant.ts`
|
||||
içindedir; oradaki kontroller yalnızca butonları gizler, asıl kontrol AppService'lerdedir):
|
||||
|
||||
| Grup | Alt yetkiler |
|
||||
| --- | --- |
|
||||
| `App.Listforms.Wizard` | `.Create`, `.Update`, `.Delete`, `.Export`, `.Import` |
|
||||
| `App.DeveloperKit.Components` | `.Create`, `.Update`, `.Delete` |
|
||||
| `App.DeveloperKit.CustomEndpoints` | `.Get`, `.Post`, `.Put`, `.Remove` (dispatcher üzerinden çağrı kapısı; endpoint bazlı User/Role/Global kuralları bunun üstünde denetlenir) |
|
||||
| `App.DeveloperKit.DynamicServices` | `.Create`, `.Edit`, `.Delete`, `.Manage`, `.TestCompile`, `.Publish`, `.ViewCode` |
|
||||
| `App.SqlQueryManager` | `.CrudEndpoints` |
|
||||
| `App.Setup.Migrate` | Migration + seed tetikleme (host tarafı) |
|
||||
| `App.{Home,About,Services,Contact}.Design` | Public site sayfa tasarım modu (`?design=1`) |
|
||||
|
||||
> **Kural:** Yetki sözleşmesi olmayan menü/route önerilmez ve eklenmez.
|
||||
|
||||
---
|
||||
|
|
@ -729,6 +976,9 @@ konfigürasyonu ile üretilir.
|
|||
|
||||
- Tailwind tabanlı tasarım sistemi (`components/ui`) + DevExtreme temaları; açık/koyu mod ve
|
||||
tema yapılandırıcı (`ThemeConfigurator`).
|
||||
- **Varsayılan mod koyudur.** `proxy/theme/theme.config.ts`: `mode` ve `navMode` `dark`, DevExtreme
|
||||
stili `dx.material.blue.dark.compact`. Kullanıcı tercihi store üzerinden saklanır; bu yalnızca
|
||||
ilk açılış varsayılanıdır.
|
||||
- Dil metinleri veritabanından gelir (`Sozsoft.Languages`); dil seçici header'dadır.
|
||||
Tenant bazlı çeviri geçersiz kılma desteklenir.
|
||||
- Layout seçenekleri: dikey menü, yatay menü, yığılmış yan menü, mobil navigasyon.
|
||||
|
|
@ -828,6 +1078,11 @@ Script sırası (`configs/deployment/scripts`):
|
|||
8-dev-build.sh / 9-dev-deploy.sh / 10-dev-migrator-true.sh → Dev ortamı
|
||||
```
|
||||
|
||||
Migrator servisi `SEED` ortam değişkeni ile çalışır ve compose dosyasında `SEED=${SEED:-true}`
|
||||
olarak tanımlıdır: değişken verilmezse seed **çalışır**. Seed dosyaları imaja gömülmez; depodaki
|
||||
`configs/seeds` klasöründen mount edilir (`App:SeedsPath`), böylece bir seed değiştiğinde imaj
|
||||
build etmek gerekmez.
|
||||
|
||||
Ayrıntılı kurulum notları: [`configs/deployment/README.md`](configs/deployment/README.md),
|
||||
[`Readme_Production.md`](configs/deployment/Readme_Production.md),
|
||||
[`Readme_Devops.md`](configs/deployment/Readme_Devops.md).
|
||||
|
|
@ -867,25 +1122,61 @@ dotnet build
|
|||
dotnet run --project src/Sozsoft.Platform.HttpApi.Host
|
||||
dotnet format --include .\modules\Sozsoft.Notifications\ --folder
|
||||
|
||||
# Kod standardı kapısı (ölü kod / stil analizörleri)
|
||||
dotnet build Sozsoft.Platform.sln --no-incremental `
|
||||
-p:EnforceCodeStyleInBuild=true -p:GenerateDocumentationFile=true
|
||||
dotnet format Sozsoft.Platform.sln --diagnostics IDE0005,IDE0161 --severity warn
|
||||
|
||||
# Migration (EntityFrameworkCore projesi içinde)
|
||||
dotnet ef migrations add <Ad>
|
||||
dotnet ef database update
|
||||
dotnet ef migrations remove
|
||||
```
|
||||
|
||||
.NET/ABP kod standardı (hedef sürümler, nullable politikası, ölü kod kuralları, modern C#
|
||||
kullanımı, ABP katman sınırları) bağlayıcı olarak
|
||||
[`.github/instructions/dotnet.instructions.md`](.github/instructions/dotnet.instructions.md)
|
||||
dosyasındadır. Standart derlemede zorlanır: `api/Directory.Build.props` analizörleri açar,
|
||||
`api/.editorconfig` kural şiddetlerini tanımlar; bu iki dosya standardın tek kaynağıdır.
|
||||
|
||||
Yeni ABP modülü eklemek için:
|
||||
`abp new Sozsoft.<Modul> -t module --no-ui -m none --database-provider ef`
|
||||
|
||||
### 21.4. Seed dosyaları
|
||||
|
||||
Yeni bir modülün nasıl kurulacağını öğrenmek için başvurulacak dosyalar:
|
||||
İki ayrı seed yüzeyi vardır ve karıştırılmamalıdır.
|
||||
|
||||
- `api/src/Sozsoft.Platform.DbMigrator/Migrations/ListFormSeeder_Saas.cs`
|
||||
- `.../Migrations/ListFormSeeder_Administration.cs`
|
||||
- `.../Migrations/MenusData.json`
|
||||
- `.../Migrations/PermissionsData.json`
|
||||
- `.../Migrations/HostData.json`
|
||||
- `.../Migrations/LanguagesData.json`
|
||||
**1. Depo içi (kod ile taşınan) seed'ler** — `api/src/Sozsoft.Platform.DbMigrator/Migrations/`.
|
||||
Platformun kendi ekranları, menüleri, yetkileri ve dil metinleri buradadır; değişiklikleri
|
||||
derlenip yayınlanır. Yeni bir modülün nasıl kurulacağını öğrenmek için başvurulacak dosyalar:
|
||||
|
||||
- `ListFormSeeder_Saas.cs`, `ListFormSeeder_Administration.cs` — ListForm + alan tanımları
|
||||
- `MenusData.json` — `Routes`, `MenuGroups`, `Menus`
|
||||
- `PermissionsData.json` — yetki grupları ve tanımları
|
||||
- `HostData.json` — ayar/entegrasyon değerleri
|
||||
- `LanguagesData.json` — dil anahtarları (EN/TR)
|
||||
- `WizardDataSeeder.cs`, `CustomComponentDataSeeder.cs`, `CrudDataSeeder.cs` — aşağıdaki
|
||||
runtime seed dosyalarını okuyup uygulayan seeder'lar
|
||||
|
||||
**2. Runtime'da üretilen seed'ler** — `configs/seeds/`. Wizard, Component Manager ve CRUD Endpoint
|
||||
Manager çalışma zamanında burayı yazar; veritabanı silinip yeniden oluşturulduğunda aynı dosyalar
|
||||
geri yüklenir. Kapsam klasörü CDN düzeniyle aynıdır ve `SeedPathResolver` üzerinden çözülür:
|
||||
|
||||
```
|
||||
configs/seeds/
|
||||
├── host/ # host kapsamı
|
||||
│ ├── wizard/ # {zaman damgası}_{Ad}.json → WizardDataSeeder
|
||||
│ ├── custom/ # {ComponentName}.json → CustomComponentDataSeeder
|
||||
│ ├── crud/ # {EntityName}.json → CrudDataSeeder
|
||||
│ ├── sql/{object,execute}/ # .sql (SQL Server)
|
||||
│ └── postgres/{object,execute}/
|
||||
├── tenants/{tenantId}/… # aynı düzen, tenant kapsamı
|
||||
└── .imports/{importId}/ # wizard import staging + backup (seeder taramalarına girmez)
|
||||
```
|
||||
|
||||
> Bu dosyalar elle de düzenlenebilir; ilgili ekrandan tekrar kaydedildiğinde yeniden üretilirler.
|
||||
> Wizard dosyası `EditFileName` ile güncellenirse sunucu önce eski dosyayı ve ürettiği kayıtları
|
||||
> siler, sonra yenisini üretir.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -913,8 +1204,11 @@ Yeni bir modülün nasıl kurulacağını öğrenmek için başvurulacak dosyala
|
|||
- `Telegram` bildirim kanalı altyapıda tanımlıdır ancak UI'da kapalıdır.
|
||||
- Depodaki `appsettings*.json` ve deployment dokümanları geliştirme değerleri/örnek kimlik
|
||||
bilgileri içerir; üretim öncesi tümü değiştirilmelidir.
|
||||
- `.github/instructions/ai.instructions.md` içinde stack ".NET 9 + ABP 9" olarak yazılıdır;
|
||||
kod tabanı **.NET 10 + ABP 10** üzerindedir. Kural bölümleri geçerliliğini korur.
|
||||
- SQL View Designer tek yönlüdür (model → T-SQL). Elle yazılmış ya da tasarımcının kanonik
|
||||
şekline uymayan bir view tanımı geri okunamaz; dialog ham SQL moduna düşer.
|
||||
- `api/test/Sozsoft.Platform.EntityFrameworkCore.Tests` şu an SQLite şema oluşturmada
|
||||
(`nvarchar(max)` → `SQLite Error 1`) kırıktır; bu kırıklık kod standardından önce de vardı ve
|
||||
ayrı bir iş olarak ele alınmalıdır.
|
||||
- Rol–yetki eşleşmeleri veritabanında müşteri ortamına göre değişir; buradaki yetki kodları
|
||||
tanımın kendisidir, atama değil.
|
||||
|
||||
|
|
|
|||
76
claude.md
76
claude.md
|
|
@ -92,3 +92,79 @@ and drop the rest. An empty or obvious heading is noise.
|
|||
7. Tenant isolation notes
|
||||
8. Validation and test checklist
|
||||
9. Rollback strategy
|
||||
|
||||
---
|
||||
|
||||
## Standing Context (read this before proposing anything)
|
||||
|
||||
These sections encode expectations the user will **not** repeat in every prompt.
|
||||
`README.md` is the technical map of the platform; this file is how to act on it.
|
||||
|
||||
### Where the truth lives
|
||||
|
||||
| Konu | Kaynak |
|
||||
| --- | --- |
|
||||
| Platform davranışı, karar sırası, çıktı sözleşmesi | `.github/instructions/ai.instructions.md` |
|
||||
| **Artefakt üretimi**: seed şemaları, enum değerleri, çalışan örnekler | `.github/instructions/lowcode.instructions.md` |
|
||||
| `api/` kod standardı (sürüm, nullable, ölü kod, modern C#, ABP katmanları) | `.github/instructions/dotnet.instructions.md` |
|
||||
| Modül/liste ekleme prosedürü, seeder dosya sorumlulukları | `.github/instructions/list.instructions.md` |
|
||||
| Ne nerede yaşıyor, hangi ekran ne yapıyor | `README.md` |
|
||||
| Yetki kodları | `PlatformConsts.AppCodes` (backend) + `constants/permission.constant.ts` (UI) |
|
||||
| Seed yolları | `SeedPathResolver` |
|
||||
|
||||
Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` → bu dosya → `README.md`.
|
||||
|
||||
### Standing defaults — do not ask again
|
||||
|
||||
1. **Yeni tablo** → tenant + tam audit kolonları varsayılan gelir (`Id`, `TenantId`,
|
||||
`CreationTime`, `CreatorId`, `LastModificationTime`, `LastModifierId`, `IsDeleted`,
|
||||
`DeletionTime`, `DeleterId`). Kullanıcı açıkça "tenant yok / audit yok" demedikçe çıkarma.
|
||||
2. **Yeni ekran** → Wizard yolu. `Definitions` altına otomatik yerleştirme yok; modüle ait yeni
|
||||
bir kök menü oluştur, `Order = max(Order) + 1`.
|
||||
3. **Yeni ekran/menü** → yetki sözleşmesi olmadan önerilmez. Menü ve route aynı ekran
|
||||
sözleşmesini göstermeli.
|
||||
4. **Kullanıcıya görünen her metin** → dil anahtarı üzerinden, EN + TR birlikte. Koda gömülü
|
||||
metin önerme.
|
||||
5. **Runtime'da üretilen her artefakt** (wizard, custom component, crud endpoint) → ilgili seed
|
||||
dosyası da yazılır/güncellenir. Veritabanı sıfırlandığında geri gelmeyen bir şey üretme.
|
||||
6. **Ekran/komponent talebi — yazılı ya da görsel** → önce ekran tarifi çıkar, sonra yolu seç
|
||||
(grid benzeri iş → SQL Query Manager + Wizard; serbest yerleşim → Custom Component),
|
||||
sonra `configs/seeds/` altına dosyaları üret. Şüphede Wizard. Ekran görüntüsü piksel
|
||||
sözleşmesi değildir; platformun kendi bileşenleriyle en yakın karşılık üretilir.
|
||||
Ayrıntı: `lowcode.instructions.md` §0.
|
||||
7. **Ekran içi hesap/koşul ihtiyacı** → önce `EditorScript` (Script Builder tarifleri), sonra
|
||||
`EditorOptions`; kod yazmak son çare.
|
||||
8. **SQL** → her zaman parametreli. String birleştirme ile sorgu kurma.
|
||||
9. **Silme** → `DeleteCommand = DefaultDeleteCommand("{Tablo}")`; ham SQL string'i yazma.
|
||||
|
||||
### Cross-cutting: touching one thing means touching these too
|
||||
|
||||
- **`scriptRecipes.ts` (TS) ↔ `Domain.Shared/Editors/*.cs`** — biri değişirse diğeri de değişir.
|
||||
İkisinin çıktısı **birebir aynı** olmak zorundadır; aksi hâlde seeder ile basılan script
|
||||
dialogda "elle düzenlenmiş" sayılır ve kural editörü kapanır.
|
||||
- **Yeni bir yetki** → `PlatformConsts.AppCodes` + `PermissionsData.json` + (UI'da kullanılacaksa)
|
||||
`permission.constant.ts` + `LanguagesData.json`.
|
||||
- **Yeni bir dil anahtarı** → `LanguagesData.json`; eklemeden önce anahtarın zaten var olup
|
||||
olmadığını kontrol et.
|
||||
- **Custom component kaydı/silinmesi** → `custom/{Name}.json`; kullandığı endpoint'ler
|
||||
`crud/{Entity}.json`.
|
||||
- **Wizard dosyası** → tek başına taşınmaz; export zip'i `wizard/`, `custom/`, `crud/` ve
|
||||
`{sql|postgres}/{object|execute}/` bağımlılıklarını da içerir.
|
||||
- **`MenusData.json`** → `Routes` bölümüne dokunma; yalnızca `MenuGroups` ve `Menus`.
|
||||
|
||||
### Definition of done
|
||||
|
||||
- Ölü kod bırakma: kullanılmayan `using`, private üye, alan, DTO, hook, servis, tip. Bir tip
|
||||
silinmeden önce **repo geneli** (api + ui + configs) referans taraması yapılır; DI ile çözülen
|
||||
tipler düz metin aramasında referanssız görünür.
|
||||
- Yorum satırına alınmış kod bloğu bırakılmaz.
|
||||
- `api/` değişikliğinden sonra: `dotnet build -p:EnforceCodeStyleInBuild=true` — 0 hata, yeni
|
||||
`IDE00xx` yok.
|
||||
- `ui/` değişikliğinden sonra: `npm run typecheck` ve `npm run lint`.
|
||||
- Yorumlar **neden**i anlatır, ne yaptığını değil. Kod zaten ne yaptığını söylüyor.
|
||||
|
||||
### Tone
|
||||
|
||||
- Türkçe soruya Türkçe cevap.
|
||||
- Platformu tanıtma, mimariyi özetleme, ne yapacağını anlatıp sonra bir de ne yaptığını özetleme.
|
||||
- Cevabın boyutu sorunun boyutu kadar olsun.
|
||||
|
|
|
|||
12
ui/src/constants/actionButton.constant.ts
Normal file
12
ui/src/constants/actionButton.constant.ts
Normal 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
|
||||
23
ui/src/sw.ts
23
ui/src/sw.ts
|
|
@ -37,7 +37,7 @@ declare const self: {
|
|||
__WB_MANIFEST: PrecacheEntry[]
|
||||
location: Location
|
||||
navigator?: { connection?: { saveData?: boolean; effectiveType?: string } }
|
||||
registration: { scope: string }
|
||||
registration: { scope: string; installing?: unknown; waiting?: unknown }
|
||||
skipWaiting: () => Promise<void>
|
||||
clients: {
|
||||
claim: () => Promise<void>
|
||||
|
|
@ -133,6 +133,16 @@ async function fetchWarmupList(): Promise<WarmupEntry[]> {
|
|||
let warmupPromise: Promise<WarmupEntry[]> | undefined
|
||||
const loadWarmupList = () => (warmupPromise ??= fetchWarmupList())
|
||||
|
||||
/**
|
||||
* Yeni bir sürüm kuruluyor ya da geçişi bekliyor mu?
|
||||
*
|
||||
* Bu kontrol ISITMA için: eski aktif worker ısıtmayı sürdürürken yenisi kurulum
|
||||
* yapıyorsa iki oturum hem bant genişliği için yarışır hem de istemcide iç içe
|
||||
* geçmiş iki ilerleme akışı üretir. Kurulum önceliklidir. (Kurulum oturumunun
|
||||
* kendisi bu kontrolü kullanamaz: kuran worker `installing`'in ta kendisidir.)
|
||||
*/
|
||||
const updatePending = () => !!(self.registration.installing || self.registration.waiting)
|
||||
|
||||
/** Veri tasarrufu açıkken ya da 2G'de ~20 MB'lık ısıtma yapılmaz. */
|
||||
function isWarmupAllowed() {
|
||||
const connection = self.navigator?.connection
|
||||
|
|
@ -367,14 +377,20 @@ async function runSession({ files, jobs, criticalCount, reusedCount, phase }: Se
|
|||
})
|
||||
}
|
||||
|
||||
// Isıtma, araya bir kurulum girdiğinde kalan işleri bırakır (bkz.
|
||||
// `updatePending`); kurulumun kendisi hiçbir koşulda kesilmez.
|
||||
const abandoned = () => phase === 'warmup' && updatePending()
|
||||
|
||||
const run = async (pool: DownloadJob[], concurrency: number, deadline?: number) => {
|
||||
let cursor = 0
|
||||
const worker = async () => {
|
||||
for (;;) {
|
||||
const next = cursor++
|
||||
if (next >= pool.length) return
|
||||
// Süre bütçesi dolduysa kalan bileşenler runtime cache'e bırakılır.
|
||||
// Süre bütçesi dolduysa ya da kurulum başladıysa kalan bileşenler
|
||||
// runtime cache'e bırakılır.
|
||||
if (deadline && Date.now() > deadline) return
|
||||
if (abandoned()) return
|
||||
|
||||
const job = pool[next]
|
||||
const file = files[job.index]
|
||||
|
|
@ -453,7 +469,8 @@ async function installAssets() {
|
|||
let warmupRunning = false
|
||||
|
||||
async function ensureWarmup() {
|
||||
if (warmupRunning || !isWarmupAllowed()) return
|
||||
// Kurulum sürüyorsa bileşenler zaten o oturumda iniyor; ikinci akış açma.
|
||||
if (warmupRunning || updatePending() || !isWarmupAllowed()) return
|
||||
warmupRunning = true
|
||||
try {
|
||||
const assetCache = await caches.open(ASSET_CACHE)
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ import { WizardComponentKindEnum, WizardFileInfoDto } from '@/proxy/admin/wizard
|
|||
import { useNavigationIcons } from '@/proxy/menus/navigation-icon.config'
|
||||
import { usePermission } from '@/utils/hooks/usePermission'
|
||||
import { WIZARD_PERMISSION } from '@/constants/permission.constant'
|
||||
import { ACTION_BUTTON_CLASS, ACTION_BUTTON_HOVER } from '@/constants/actionButton.constant'
|
||||
import WizardImportDialog from './WizardImportDialog'
|
||||
|
||||
/** Layout tercihi diger listelerle ayni yerde (admin.lists.states) saklanir. */
|
||||
|
|
@ -178,55 +179,55 @@ const WizardFileManager = () => {
|
|||
{canUpdate && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="solid"
|
||||
color="blue-600"
|
||||
variant="plain"
|
||||
shape="none"
|
||||
title={translate('::App.Platform.Edit')}
|
||||
icon={<FaEdit className="h-4 w-4" />}
|
||||
className={`${ACTION_BUTTON_CLASS} ${ACTION_BUTTON_HOVER.blue}`}
|
||||
onClick={() =>
|
||||
navigate(ROUTES_ENUM.protected.saas.listFormManagement.wizard, {
|
||||
state: { editFileName: f.fileName },
|
||||
})
|
||||
}
|
||||
>
|
||||
<FaEdit className="w-4 h-4" />
|
||||
</Button>
|
||||
/>
|
||||
)}
|
||||
{/* Menünün açtığı adres yeni sekmede açılır; adres yoksa buton anlamsızdır. */}
|
||||
{f.menuUrl && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="solid"
|
||||
color="gray-600"
|
||||
variant="plain"
|
||||
shape="none"
|
||||
title={translate('::App.Platform.OpenUrl')}
|
||||
icon={<FaExternalLinkAlt className="h-4 w-4" />}
|
||||
className={`${ACTION_BUTTON_CLASS} ${ACTION_BUTTON_HOVER.slate}`}
|
||||
onClick={() => window.open(f.menuUrl, '_blank', 'noopener,noreferrer')}
|
||||
>
|
||||
<FaExternalLinkAlt className="w-4 h-4" />
|
||||
</Button>
|
||||
/>
|
||||
)}
|
||||
{canExport && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="solid"
|
||||
color="green-600"
|
||||
variant="plain"
|
||||
shape="none"
|
||||
title={translate('::App.ListForm.WizardFileExport')}
|
||||
loading={exportingFile === f.fileName}
|
||||
icon={<FaFileExport className="h-4 w-4" />}
|
||||
className={`${ACTION_BUTTON_CLASS} ${ACTION_BUTTON_HOVER.green}`}
|
||||
onClick={() => handleExport(f)}
|
||||
>
|
||||
<FaFileExport className="w-4 h-4" />
|
||||
</Button>
|
||||
/>
|
||||
)}
|
||||
{canDelete && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="solid"
|
||||
color="red-600"
|
||||
variant="plain"
|
||||
shape="none"
|
||||
title={translate('::App.Platform.Delete')}
|
||||
loading={deletingFile === f.fileName}
|
||||
icon={<FaTrash className="h-4 w-4" />}
|
||||
className={`${ACTION_BUTTON_CLASS} ${ACTION_BUTTON_HOVER.red}`}
|
||||
onClick={() =>
|
||||
setConfirm({ fileName: f.fileName, wizardName: f.wizardName || f.fileName })
|
||||
}
|
||||
>
|
||||
<FaTrash className="w-4 h-4" />
|
||||
</Button>
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import Select from '@/components/ui/Select'
|
|||
import type { CustomComponent } from '@/proxy/developerKit/models'
|
||||
import { useStoreActions, useStoreState } from '@/store/store'
|
||||
import type { ListViewLayoutType } from '../admin/listForm/edit/types'
|
||||
import { ACTION_BUTTON_CLASS, ACTION_BUTTON_HOVER } from '@/constants/actionButton.constant'
|
||||
|
||||
/** Layout tercihi diger listelerle ayni yerde (admin.lists.states) saklanir. */
|
||||
const VIEW_STATE_CODE = 'developerkit-components'
|
||||
|
|
@ -146,53 +147,53 @@ const ComponentManager: React.FC = () => {
|
|||
{canUpdate && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="solid"
|
||||
color="blue-600"
|
||||
variant="plain"
|
||||
shape="none"
|
||||
title={translate('::App.Platform.Edit')}
|
||||
icon={<FaRegEdit className="h-4 w-4" />}
|
||||
className={`${ACTION_BUTTON_CLASS} ${ACTION_BUTTON_HOVER.blue}`}
|
||||
onClick={() =>
|
||||
window.open(
|
||||
ROUTES_ENUM.protected.saas.developerKit.componentsEdit.replace(':id', component.id),
|
||||
'_blank',
|
||||
)
|
||||
}
|
||||
>
|
||||
<FaRegEdit className="w-4 h-4" />
|
||||
</Button>
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
type="button"
|
||||
variant="solid"
|
||||
color="gray-600"
|
||||
variant="plain"
|
||||
shape="none"
|
||||
disabled={!component.isActive || !component.routePath?.trim()}
|
||||
title={translate('::App.Platform.OpenUrl')}
|
||||
icon={<FaExternalLinkAlt className="h-4 w-4" />}
|
||||
className={`${ACTION_BUTTON_CLASS} ${ACTION_BUTTON_HOVER.slate}`}
|
||||
onClick={() => {
|
||||
const routePath = component.routePath.startsWith('/')
|
||||
? component.routePath
|
||||
: `/${component.routePath}`
|
||||
window.open(routePath, '_blank')
|
||||
}}
|
||||
>
|
||||
<FaExternalLinkAlt className="w-4 h-4" />
|
||||
</Button>
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="solid"
|
||||
color="green-600"
|
||||
variant="plain"
|
||||
shape="none"
|
||||
title={translate('::App.Platform.View')}
|
||||
icon={<FaCog className="h-4 w-4" />}
|
||||
className={`${ACTION_BUTTON_CLASS} ${ACTION_BUTTON_HOVER.green}`}
|
||||
onClick={() => setEditorComponentId(component.id)}
|
||||
>
|
||||
<FaCog className="w-4 h-4" />
|
||||
</Button>
|
||||
/>
|
||||
{canDelete && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="solid"
|
||||
color="red-600"
|
||||
variant="plain"
|
||||
shape="none"
|
||||
title={translate('::App.Platform.Delete')}
|
||||
icon={<FaTrashAlt className="h-4 w-4" />}
|
||||
className={`${ACTION_BUTTON_CLASS} ${ACTION_BUTTON_HOVER.red}`}
|
||||
onClick={() => handleDelete(component.id)}
|
||||
>
|
||||
<FaTrashAlt className="w-4 h-4" />
|
||||
</Button>
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useState, useEffect } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Link, useNavigate } from 'react-router-dom'
|
||||
import {
|
||||
FaPlus,
|
||||
FaSearch,
|
||||
|
|
@ -10,6 +10,8 @@ import {
|
|||
FaCode,
|
||||
FaSpinner,
|
||||
FaExternalLinkAlt,
|
||||
FaTh,
|
||||
FaList,
|
||||
} from 'react-icons/fa'
|
||||
import Widget from '@/components/common/Widget'
|
||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
|
|
@ -19,9 +21,16 @@ import { ROUTES_ENUM } from '@/routes/route.constant'
|
|||
import Button from '@/components/ui/Button'
|
||||
import Input from '@/components/ui/Input'
|
||||
import Select from '@/components/ui/Select'
|
||||
import { useStoreActions, useStoreState } from '@/store/store'
|
||||
import type { ListViewLayoutType } from '../admin/listForm/edit/types'
|
||||
import { ACTION_BUTTON_CLASS, ACTION_BUTTON_HOVER } from '@/constants/actionButton.constant'
|
||||
|
||||
/** Layout tercihi diger listelerle ayni yerde (admin.lists.states) saklanir. */
|
||||
const VIEW_STATE_CODE = 'developerkit-dynamicservices'
|
||||
|
||||
const DynamicServiceManager: React.FC = () => {
|
||||
const { translate } = useLocalization()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const filterStatusOptions = [
|
||||
{ value: 'all' as const, label: translate('::App.StaticLookup.All') },
|
||||
|
|
@ -37,6 +46,13 @@ const DynamicServiceManager: React.FC = () => {
|
|||
const [searchTerm, setSearchTerm] = useState('')
|
||||
const [filterStatus, setFilterStatus] = useState<'all' | 'Success' | 'Failed' | 'Pending'>('all')
|
||||
|
||||
const { states } = useStoreState((state) => state.admin.lists)
|
||||
const { setStates } = useStoreActions((a) => a.admin.lists)
|
||||
const viewMode: ListViewLayoutType =
|
||||
states.find((s) => s.listFormCode === VIEW_STATE_CODE)?.layout === 'grid' ? 'grid' : 'card'
|
||||
const setViewMode = (layout: ListViewLayoutType) =>
|
||||
setStates({ listFormCode: VIEW_STATE_CODE, layout })
|
||||
|
||||
useEffect(() => {
|
||||
loadServices()
|
||||
}, [])
|
||||
|
|
@ -79,15 +95,53 @@ const DynamicServiceManager: React.FC = () => {
|
|||
})
|
||||
|
||||
const statusBadge = (status: string) => {
|
||||
if (status === 'Success') return 'bg-emerald-100 text-emerald-700'
|
||||
if (status === 'Failed') return 'bg-red-100 text-red-700'
|
||||
return 'bg-yellow-100 text-yellow-700'
|
||||
if (status === 'Success')
|
||||
return 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300'
|
||||
if (status === 'Failed') return 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300'
|
||||
return 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-300'
|
||||
}
|
||||
|
||||
const openSwagger = () => {
|
||||
window.open(`${import.meta.env.VITE_API_URL}/swagger/index.html`, '_blank')
|
||||
}
|
||||
|
||||
const renderStatusBadge = (service: DynamicServiceDto) => (
|
||||
<span
|
||||
className={`inline-block px-2 py-0.5 text-xs rounded-full font-medium whitespace-nowrap ${statusBadge(
|
||||
service.compilationStatus,
|
||||
)}`}
|
||||
>
|
||||
{service.compilationStatus} · v{service.version}
|
||||
</span>
|
||||
)
|
||||
|
||||
const renderActions = (service: DynamicServiceDto) => (
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
type="button"
|
||||
variant="plain"
|
||||
shape="none"
|
||||
title={translate('::App.Platform.Edit')}
|
||||
icon={<FaRegEdit className="h-4 w-4" />}
|
||||
className={`${ACTION_BUTTON_CLASS} ${ACTION_BUTTON_HOVER.blue}`}
|
||||
onClick={() =>
|
||||
navigate(
|
||||
ROUTES_ENUM.protected.saas.developerKit.dynamicServicesEdit.replace(':id', service.id),
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="plain"
|
||||
shape="none"
|
||||
title={translate('::App.Platform.Delete')}
|
||||
icon={<FaTrashAlt className="h-4 w-4" />}
|
||||
className={`${ACTION_BUTTON_CLASS} ${ACTION_BUTTON_HOVER.red}`}
|
||||
onClick={() => deleteService(service.id)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<PageTitle title={translate('::' + 'App.DeveloperKit.DynamicServices')} />
|
||||
|
|
@ -145,8 +199,8 @@ const DynamicServiceManager: React.FC = () => {
|
|||
type="text"
|
||||
placeholder={translate('::App.DeveloperKitDynamicServices.SearchPlaceholder')}
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full pl-10 pr-4 py-2 border border-slate-300 dark:border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100"
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 w-full lg:w-auto">
|
||||
|
|
@ -161,11 +215,11 @@ const DynamicServiceManager: React.FC = () => {
|
|||
<div className="w-full sm:w-auto">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={openSwagger}
|
||||
variant="solid"
|
||||
color="green-600"
|
||||
icon={<FaExternalLinkAlt className="h-3 w-3" />}
|
||||
className="w-full !h-auto !items-center !justify-center gap-2 whitespace-nowrap !rounded-lg px-4 py-2 text-white transition-colors hover:!bg-green-700 dark:!bg-green-700 dark:hover:!bg-green-800 sm:w-auto"
|
||||
onClick={openSwagger}
|
||||
>
|
||||
Swagger
|
||||
</Button>
|
||||
|
|
@ -179,6 +233,22 @@ const DynamicServiceManager: React.FC = () => {
|
|||
{translate('::App.DeveloperKitDynamicServices.NewService')}
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
type="button"
|
||||
icon={<FaTh />}
|
||||
variant={viewMode === 'card' ? 'solid' : 'default'}
|
||||
title={translate('::App.Platform.CardView')}
|
||||
onClick={() => setViewMode('card')}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
icon={<FaList />}
|
||||
variant={viewMode === 'grid' ? 'solid' : 'default'}
|
||||
title={translate('::App.Platform.ListView')}
|
||||
onClick={() => setViewMode('grid')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* List */}
|
||||
|
|
@ -187,79 +257,138 @@ const DynamicServiceManager: React.FC = () => {
|
|||
<FaSpinner className="w-8 h-8 animate-spin text-slate-400" />
|
||||
</div>
|
||||
) : filteredServices.length > 0 ? (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||
{filteredServices.map((service) => (
|
||||
<div
|
||||
key={service.id}
|
||||
className="bg-white dark:bg-gray-900 rounded-lg border border-slate-200 dark:border-gray-700 shadow-sm hover:shadow-md transition-shadow"
|
||||
>
|
||||
<div className="p-6">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h3 className="text-base font-semibold text-slate-900 dark:text-gray-100">
|
||||
{service.name}
|
||||
</h3>
|
||||
<div
|
||||
className={`w-2 h-2 rounded-full ${
|
||||
service.compilationStatus === 'Success'
|
||||
? 'bg-emerald-500'
|
||||
: 'bg-slate-300 dark:bg-gray-700'
|
||||
}`}
|
||||
/>
|
||||
viewMode === 'card' ? (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||
{filteredServices.map((service) => (
|
||||
<div
|
||||
key={service.id}
|
||||
className="bg-white dark:bg-gray-900 rounded-lg border border-slate-200 dark:border-gray-700 shadow-sm hover:shadow-md transition-shadow"
|
||||
>
|
||||
<div className="p-4">
|
||||
<div className="flex items-start justify-between">
|
||||
{/* Sol taraf */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h3
|
||||
className="text-base font-semibold text-slate-900 dark:text-gray-100 truncate"
|
||||
title={service.name}
|
||||
>
|
||||
{service.name}
|
||||
</h3>
|
||||
<div
|
||||
className={`w-2 h-2 shrink-0 rounded-full ${
|
||||
service.isActive ? 'bg-emerald-500' : 'bg-slate-300 dark:bg-gray-700'
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{service.displayName && (
|
||||
<p
|
||||
className="text-slate-600 dark:text-gray-300 text-sm mb-2 truncate"
|
||||
title={service.displayName}
|
||||
>
|
||||
{service.displayName}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="mb-2">{renderStatusBadge(service)}</div>
|
||||
|
||||
{service.description && (
|
||||
<p className="text-slate-600 dark:text-gray-300 text-sm mb-2 line-clamp-2">
|
||||
{service.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{service.displayName && (
|
||||
<p className="text-slate-500 dark:text-gray-400 text-sm mb-1">
|
||||
{service.displayName}
|
||||
</p>
|
||||
)}
|
||||
<span
|
||||
className={`inline-block px-2 py-0.5 text-xs rounded-full font-medium mb-3 ${statusBadge(service.compilationStatus)} dark:bg-opacity-80`}
|
||||
>
|
||||
{service.compilationStatus} · v{service.version}
|
||||
</span>
|
||||
{service.description && (
|
||||
<p className="text-slate-500 dark:text-gray-400 text-sm">
|
||||
{service.description}
|
||||
</p>
|
||||
|
||||
{/* Sag taraf */}
|
||||
{service.lastSuccessfulCompilation && (
|
||||
<div className="flex items-center gap-1 text-xs text-slate-500 dark:text-gray-400 ml-2 whitespace-nowrap">
|
||||
<FaCalendarAlt className="w-3 h-3" />
|
||||
<span>
|
||||
{new Date(service.lastSuccessfulCompilation).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{service.lastSuccessfulCompilation && (
|
||||
<div className="flex items-center gap-1 text-xs text-slate-400 dark:text-gray-400 sm:ml-4 whitespace-nowrap">
|
||||
<FaCalendarAlt className="w-3 h-3" />
|
||||
<span>
|
||||
{new Date(service.lastSuccessfulCompilation).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center justify-end pt-3 border-t border-slate-100 dark:border-gray-700 gap-1 mt-4">
|
||||
<Link
|
||||
to={ROUTES_ENUM.protected.saas.developerKit.dynamicServicesEdit.replace(
|
||||
':id',
|
||||
service.id,
|
||||
)}
|
||||
className="p-2 text-slate-500 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 hover:bg-blue-50 dark:hover:bg-gray-800 rounded transition-colors"
|
||||
title={translate('::App.Platform.Edit')}
|
||||
>
|
||||
<FaRegEdit className="w-4 h-4" />
|
||||
</Link>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => deleteService(service.id)}
|
||||
variant="plain"
|
||||
shape="none"
|
||||
icon={<FaTrashAlt className="h-4 w-4" />}
|
||||
className="!h-8 !w-8 !rounded !p-0 text-slate-500 transition-colors hover:!bg-red-50 hover:text-red-600 dark:text-gray-400 dark:hover:!bg-gray-800 dark:hover:text-red-400"
|
||||
title={translate('::App.Platform.Delete')}
|
||||
/>
|
||||
{/* Actions */}
|
||||
<div className="flex items-center justify-end gap-1 pt-2 border-t border-slate-100 dark:border-gray-700">
|
||||
{renderActions(service)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-white dark:bg-gray-900 rounded-lg border border-slate-200 dark:border-gray-700 shadow-sm overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-slate-50 dark:bg-gray-800 text-slate-600 dark:text-gray-300">
|
||||
<tr>
|
||||
<th className="text-left font-medium px-4 py-2">
|
||||
{translate('::App.Platform.Name')}
|
||||
</th>
|
||||
<th className="text-left font-medium px-4 py-2">
|
||||
{translate('::App.Platform.DisplayName')}
|
||||
</th>
|
||||
<th className="text-left font-medium px-4 py-2">
|
||||
{translate('::App.Platform.Description')}
|
||||
</th>
|
||||
<th className="text-left font-medium px-4 py-2 whitespace-nowrap">
|
||||
{translate('::App.Listform.ListformField.Version')}
|
||||
</th>
|
||||
<th className="text-left font-medium px-4 py-2 whitespace-nowrap">
|
||||
{translate('::App.Platform.Modified')}
|
||||
</th>
|
||||
<th className="text-left font-medium px-4 py-2">
|
||||
{translate('::App.Platform.Status')}
|
||||
</th>
|
||||
<th className="text-right font-medium px-4 py-2">
|
||||
{translate('::App.Platform.Actions')}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredServices.map((service) => (
|
||||
<tr
|
||||
key={service.id}
|
||||
className="border-t border-slate-100 dark:border-gray-700 hover:bg-slate-50 dark:hover:bg-gray-800"
|
||||
>
|
||||
<td className="px-4 py-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className={`w-2 h-2 shrink-0 rounded-full ${
|
||||
service.isActive ? 'bg-emerald-500' : 'bg-slate-300 dark:bg-gray-700'
|
||||
}`}
|
||||
/>
|
||||
<span className="font-medium text-slate-900 dark:text-gray-100">
|
||||
{service.name}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-2 text-slate-600 dark:text-gray-300">
|
||||
{service.displayName}
|
||||
</td>
|
||||
<td className="px-4 py-2 text-slate-600 dark:text-gray-300">
|
||||
{service.description}
|
||||
</td>
|
||||
<td className="px-4 py-2 text-slate-600 dark:text-gray-300 whitespace-nowrap">
|
||||
v{service.version}
|
||||
</td>
|
||||
<td className="px-4 py-2 text-slate-500 dark:text-gray-400 whitespace-nowrap">
|
||||
{service.lastSuccessfulCompilation
|
||||
? new Date(service.lastSuccessfulCompilation).toLocaleDateString()
|
||||
: ''}
|
||||
</td>
|
||||
<td className="px-4 py-2">{renderStatusBadge(service)}</td>
|
||||
<td className="px-4 py-2">
|
||||
<div className="flex justify-end">{renderActions(service)}</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="text-center py-12">
|
||||
<div className="bg-slate-100 dark:bg-gray-800 rounded-full w-16 h-16 flex items-center justify-center mx-auto mb-4">
|
||||
|
|
|
|||
|
|
@ -1048,7 +1048,7 @@ const ObjectListEditor = ({
|
|||
const { translate } = useLocalization()
|
||||
const items = Array.isArray(value) ? (value as Array<Record<string, unknown>>) : []
|
||||
const inputClass =
|
||||
'w-full rounded border border-slate-300 bg-white px-2 py-1.5 text-[10px] text-slate-800 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100'
|
||||
'w-full rounded border border-slate-300 bg-white px-2 py-1.5 text-[10px] text-slate-800 outline-none focus:border-sky-500 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100'
|
||||
|
||||
const replaceItem = (index: number, next: Record<string, unknown>) =>
|
||||
onChange(items.map((item, itemIndex) => (itemIndex === index ? next : item)))
|
||||
|
|
@ -1066,7 +1066,7 @@ const ObjectListEditor = ({
|
|||
{items.map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="space-y-1.5 rounded-md border border-slate-200 p-2 dark:border-slate-700"
|
||||
className="space-y-1.5 rounded-md border border-slate-200 p-2 dark:border-gray-700"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[10px] font-semibold text-slate-500">
|
||||
|
|
@ -1104,7 +1104,7 @@ const ObjectListEditor = ({
|
|||
{item.src ? (
|
||||
<img
|
||||
alt=""
|
||||
className="h-16 w-full rounded border border-slate-200 object-cover dark:border-slate-700"
|
||||
className="h-16 w-full rounded border border-slate-200 object-cover dark:border-gray-700"
|
||||
src={String(item.thumbnail || item.src)}
|
||||
/>
|
||||
) : null}
|
||||
|
|
@ -1132,7 +1132,7 @@ const ObjectListEditor = ({
|
|||
</div>
|
||||
))}
|
||||
<button
|
||||
className="flex w-full items-center justify-center gap-1 rounded border border-dashed border-slate-300 px-2 py-1.5 text-[10px] font-semibold text-sky-600 hover:border-sky-400 dark:border-slate-700"
|
||||
className="flex w-full items-center justify-center gap-1 rounded border border-dashed border-slate-300 px-2 py-1.5 text-[10px] font-semibold text-sky-600 hover:border-sky-400 dark:border-gray-700"
|
||||
type="button"
|
||||
onClick={() => onChange([...items, { src: '' }])}
|
||||
>
|
||||
|
|
@ -1160,12 +1160,12 @@ const PropertyEditor = ({
|
|||
const { translate } = useLocalization()
|
||||
const [styleModalOpen, setStyleModalOpen] = useState(false)
|
||||
const inputClass =
|
||||
'w-full rounded-md border border-slate-300 bg-white px-2.5 py-2 text-xs text-slate-800 outline-none transition focus:border-sky-500 focus:ring-2 focus:ring-sky-100 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100'
|
||||
'w-full rounded-md border border-slate-300 bg-white px-2.5 py-2 text-xs text-slate-800 outline-none transition focus:border-sky-500 focus:ring-2 focus:ring-sky-100 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100'
|
||||
|
||||
if (type === 'boolean') {
|
||||
return (
|
||||
<label className="flex cursor-pointer items-center justify-between rounded-md border border-slate-200 px-2.5 py-2 dark:border-slate-700">
|
||||
<span className="text-xs text-slate-600 dark:text-slate-300">
|
||||
<label className="flex cursor-pointer items-center justify-between rounded-md border border-slate-200 px-2.5 py-2 dark:border-gray-700">
|
||||
<span className="text-xs text-slate-600 dark:text-gray-300">
|
||||
{value ? 'Enabled' : 'Disabled'}
|
||||
</span>
|
||||
<Input
|
||||
|
|
@ -3429,20 +3429,20 @@ const VisualComponentDesigner = () => {
|
|||
<FaSearch className="pointer-events-none absolute left-2.5 top-1/2 -translate-y-1/2 text-[10px] text-slate-400" />
|
||||
<Input
|
||||
unstyle
|
||||
className="w-full rounded-md border border-slate-300 bg-white py-2 pl-7 pr-2.5 text-xs dark:border-slate-700 dark:bg-slate-900"
|
||||
className="w-full rounded-md border border-slate-300 bg-white py-2 pl-7 pr-2.5 text-xs dark:border-gray-700 dark:bg-gray-900"
|
||||
placeholder={translate('::App.DeveloperKitComponentDesigner.SearchEndpoint')}
|
||||
type="search"
|
||||
value={endpointSearch}
|
||||
onChange={(event) => setEndpointSearch(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 rounded-md bg-slate-100 p-1 dark:bg-slate-950">
|
||||
<div className="flex items-center gap-1 rounded-md bg-slate-100 p-1 dark:bg-gray-950">
|
||||
{(['all', ...DESIGNER_HTTP_METHODS] as const).map((method) => (
|
||||
<button
|
||||
key={method}
|
||||
className={`rounded px-2 py-1 text-[10px] font-semibold uppercase ${
|
||||
endpointMethodFilter === method
|
||||
? 'bg-white text-sky-700 shadow-sm dark:bg-slate-800 dark:text-sky-300'
|
||||
? 'bg-white text-sky-700 shadow-sm dark:bg-gray-800 dark:text-sky-300'
|
||||
: 'text-slate-500'
|
||||
}`}
|
||||
type="button"
|
||||
|
|
@ -3464,12 +3464,12 @@ const VisualComponentDesigner = () => {
|
|||
/>
|
||||
{translate('::App.DeveloperKitComponentDesigner.AttachedOnly')}
|
||||
</label>
|
||||
<span className="shrink-0 rounded bg-slate-100 px-2 py-1 text-[10px] font-semibold text-slate-500 dark:bg-slate-950">
|
||||
<span className="shrink-0 rounded bg-slate-100 px-2 py-1 text-[10px] font-semibold text-slate-500 dark:bg-gray-950">
|
||||
{visibleSources.length} / {dataSourceCatalog.length}
|
||||
</span>
|
||||
</div>
|
||||
{endpointCatalogLoading && !visibleSources.length ? (
|
||||
<p className="rounded-lg border border-dashed border-slate-300 p-3 text-center text-xs text-slate-500 dark:border-slate-700">
|
||||
<p className="rounded-lg border border-dashed border-slate-300 p-3 text-center text-xs text-slate-500 dark:border-gray-700">
|
||||
{translate('::App.DeveloperKitComponentDesigner.LoadingSavedEndpoints')}
|
||||
</p>
|
||||
) : visibleSources.length ? (
|
||||
|
|
@ -3483,10 +3483,10 @@ const VisualComponentDesigner = () => {
|
|||
{visibleSources.map((item) => (
|
||||
<div
|
||||
key={item.key}
|
||||
className="flex items-center gap-2 rounded-lg border border-slate-200 bg-white p-2.5 dark:border-slate-800 dark:bg-slate-900"
|
||||
className="flex items-center gap-2 rounded-lg border border-slate-200 bg-white p-2.5 dark:border-gray-700 dark:bg-gray-900"
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-xs font-semibold text-slate-700 dark:text-slate-200">
|
||||
<div className="truncate text-xs font-semibold text-slate-700 dark:text-gray-200">
|
||||
{item.source.name}
|
||||
</div>
|
||||
<div className="truncate font-mono text-[9px] text-sky-600">
|
||||
|
|
@ -3514,7 +3514,7 @@ const VisualComponentDesigner = () => {
|
|||
{item.originType === 'component' && (
|
||||
<>
|
||||
<button
|
||||
className="rounded-md border border-slate-200 p-1.5 text-slate-500 hover:border-amber-400 hover:text-amber-600 dark:border-slate-700"
|
||||
className="rounded-md border border-slate-200 p-1.5 text-slate-500 hover:border-amber-400 hover:text-amber-600 dark:border-gray-700"
|
||||
title={translate('::App.DeveloperKitComponentDesigner.EditEndpoint')}
|
||||
type="button"
|
||||
onClick={() => openCatalogSourceEditor(item)}
|
||||
|
|
@ -3522,7 +3522,7 @@ const VisualComponentDesigner = () => {
|
|||
<FaEdit />
|
||||
</button>
|
||||
<button
|
||||
className="rounded-md border border-slate-200 p-1.5 text-slate-500 hover:border-red-400 hover:text-red-600 dark:border-slate-700"
|
||||
className="rounded-md border border-slate-200 p-1.5 text-slate-500 hover:border-red-400 hover:text-red-600 dark:border-gray-700"
|
||||
title={translate('::App.DeveloperKitComponentDesigner.DeleteEndpoint')}
|
||||
type="button"
|
||||
onClick={() => void deleteCatalogSource(item)}
|
||||
|
|
@ -3536,7 +3536,7 @@ const VisualComponentDesigner = () => {
|
|||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="rounded-lg border border-dashed border-slate-300 p-3 text-center text-xs leading-5 text-slate-500 dark:border-slate-700">
|
||||
<p className="rounded-lg border border-dashed border-slate-300 p-3 text-center text-xs leading-5 text-slate-500 dark:border-gray-700">
|
||||
{translate(
|
||||
hasFilter
|
||||
? '::App.DeveloperKitComponentDesigner.NoMatchingEndpoint'
|
||||
|
|
@ -3558,17 +3558,17 @@ const VisualComponentDesigner = () => {
|
|||
if (!isOptionDataComponent(selectedNode?.type) && !isTabularDataComponent(selectedNode?.type))
|
||||
return null
|
||||
return (
|
||||
<div className="rounded-lg border border-slate-200 p-3 dark:border-slate-800">
|
||||
<div className="rounded-lg border border-slate-200 p-3 dark:border-gray-700">
|
||||
<div className="mb-2 text-[10px] font-semibold uppercase tracking-wider text-slate-400">
|
||||
{translate('::App.Listform.ListformField.DataSourceType')}
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-1 rounded-md bg-slate-100 p-1 dark:bg-slate-950">
|
||||
<div className="grid grid-cols-2 gap-1 rounded-md bg-slate-100 p-1 dark:bg-gray-950">
|
||||
{(['static', 'endpoint'] as const).map((mode) => (
|
||||
<button
|
||||
key={mode}
|
||||
className={`rounded px-2 py-1.5 text-[10px] font-semibold ${
|
||||
selectDataMode === mode
|
||||
? 'bg-white text-sky-700 shadow-sm dark:bg-slate-800 dark:text-sky-300'
|
||||
? 'bg-white text-sky-700 shadow-sm dark:bg-gray-800 dark:text-sky-300'
|
||||
: 'text-slate-500'
|
||||
}`}
|
||||
type="button"
|
||||
|
|
@ -3589,7 +3589,7 @@ const VisualComponentDesigner = () => {
|
|||
const renderSelectDataConfiguration = () => {
|
||||
if (!isOptionDataComponent(selectedNode?.type)) return null
|
||||
return (
|
||||
<div className="rounded-lg border border-slate-200 p-3 dark:border-slate-800">
|
||||
<div className="rounded-lg border border-slate-200 p-3 dark:border-gray-700">
|
||||
{selectDataMode === 'static' ? (
|
||||
<div className="space-y-2">
|
||||
<div className="mb-2 text-[10px] font-semibold uppercase tracking-wider text-slate-400">
|
||||
|
|
@ -3603,7 +3603,7 @@ const VisualComponentDesigner = () => {
|
|||
aria-label={translate('::App.DeveloperKitComponentDesigner.OptionLabelAria', {
|
||||
index: index + 1,
|
||||
})}
|
||||
className="min-w-0 rounded border border-slate-300 bg-white px-2 py-1.5 text-[10px] dark:border-slate-700 dark:bg-slate-900"
|
||||
className="min-w-0 rounded border border-slate-300 bg-white px-2 py-1.5 text-[10px] dark:border-gray-700 dark:bg-gray-900"
|
||||
placeholder={translate('::App.DeveloperKitComponentDesigner.LabelHeader')}
|
||||
value={option.label}
|
||||
onChange={(event) =>
|
||||
|
|
@ -3621,7 +3621,7 @@ const VisualComponentDesigner = () => {
|
|||
aria-label={translate('::App.DeveloperKitComponentDesigner.OptionValueAria', {
|
||||
index: index + 1,
|
||||
})}
|
||||
className="min-w-0 rounded border border-slate-300 bg-white px-2 py-1.5 text-[10px] dark:border-slate-700 dark:bg-slate-900"
|
||||
className="min-w-0 rounded border border-slate-300 bg-white px-2 py-1.5 text-[10px] dark:border-gray-700 dark:bg-gray-900"
|
||||
placeholder={translate('::App.Listform.ListformField.Value')}
|
||||
value={String(option.value ?? '')}
|
||||
onChange={(event) =>
|
||||
|
|
@ -3664,7 +3664,7 @@ const VisualComponentDesigner = () => {
|
|||
{selectedNode?.type === 'Tabs' && (
|
||||
<Input
|
||||
unstyle
|
||||
className="w-full min-w-0 rounded border border-slate-300 bg-white px-2 py-1.5 text-[10px] dark:border-slate-700 dark:bg-slate-900"
|
||||
className="w-full min-w-0 rounded border border-slate-300 bg-white px-2 py-1.5 text-[10px] dark:border-gray-700 dark:bg-gray-900"
|
||||
placeholder={translate(
|
||||
'::App.DeveloperKitComponentDesigner.TabContentPlaceholder',
|
||||
)}
|
||||
|
|
@ -3683,7 +3683,7 @@ const VisualComponentDesigner = () => {
|
|||
</div>
|
||||
))}
|
||||
<button
|
||||
className="flex w-full items-center justify-center gap-1 rounded border border-dashed border-slate-300 px-2 py-1.5 text-[10px] font-semibold text-sky-600 hover:border-sky-400 dark:border-slate-700"
|
||||
className="flex w-full items-center justify-center gap-1 rounded border border-dashed border-slate-300 px-2 py-1.5 text-[10px] font-semibold text-sky-600 hover:border-sky-400 dark:border-gray-700"
|
||||
type="button"
|
||||
onClick={() =>
|
||||
updateStaticSelectOptions([
|
||||
|
|
@ -3856,7 +3856,7 @@ const VisualComponentDesigner = () => {
|
|||
</>
|
||||
)}
|
||||
{activeDataSample === undefined && (
|
||||
<p className="rounded bg-slate-50 p-2 text-[10px] leading-4 text-slate-500 dark:bg-slate-950">
|
||||
<p className="rounded bg-slate-50 p-2 text-[10px] leading-4 text-slate-500 dark:bg-gray-950">
|
||||
{translate('::App.DeveloperKitComponentDesigner.SelectEndpointFirst')}
|
||||
</p>
|
||||
)}
|
||||
|
|
@ -3873,7 +3873,7 @@ const VisualComponentDesigner = () => {
|
|||
const draft = staticCollectionDrafts[selectedId] ?? JSON.stringify(currentItems, null, 2)
|
||||
const error = staticCollectionErrors[selectedId]
|
||||
return (
|
||||
<div className="rounded-lg border border-slate-200 p-3 dark:border-slate-800">
|
||||
<div className="rounded-lg border border-slate-200 p-3 dark:border-gray-700">
|
||||
<div className="mb-2 text-[10px] font-semibold uppercase tracking-wider text-slate-400">
|
||||
{translate('::App.DeveloperKitComponentDesigner.StaticRowsJson')}
|
||||
</div>
|
||||
|
|
@ -3881,7 +3881,7 @@ const VisualComponentDesigner = () => {
|
|||
{translate('::App.DeveloperKitComponentDesigner.StaticRowsHint')}
|
||||
</p>
|
||||
<textarea
|
||||
className="min-h-40 w-full rounded-md border border-slate-300 bg-white p-2 font-mono text-[10px] leading-4 dark:border-slate-700 dark:bg-slate-900"
|
||||
className="min-h-40 w-full rounded-md border border-slate-300 bg-white p-2 font-mono text-[10px] leading-4 dark:border-gray-700 dark:bg-gray-900"
|
||||
spellCheck={false}
|
||||
value={draft}
|
||||
onChange={(event) => updateStaticCollection(event.target.value)}
|
||||
|
|
@ -3915,7 +3915,7 @@ const VisualComponentDesigner = () => {
|
|||
: Object.keys(gridColumnLookups)
|
||||
if (!columns.length) return null
|
||||
return (
|
||||
<div className="rounded-lg border border-slate-200 p-3 dark:border-slate-800">
|
||||
<div className="rounded-lg border border-slate-200 p-3 dark:border-gray-700">
|
||||
<div className="mb-1 text-[10px] font-semibold uppercase tracking-wider text-slate-400">
|
||||
{translate('::App.DeveloperKitComponentDesigner.ColumnDisplay')}
|
||||
</div>
|
||||
|
|
@ -3937,7 +3937,7 @@ const VisualComponentDesigner = () => {
|
|||
return (
|
||||
<div
|
||||
key={column}
|
||||
className="rounded border border-slate-200 bg-slate-50 p-2 dark:border-slate-800 dark:bg-slate-950"
|
||||
className="rounded border border-slate-200 bg-slate-50 p-2 dark:border-gray-700 dark:bg-gray-950"
|
||||
>
|
||||
<code className="block truncate text-[10px] text-sky-700 dark:text-sky-300">
|
||||
{column}
|
||||
|
|
@ -3947,7 +3947,7 @@ const VisualComponentDesigner = () => {
|
|||
renamed. Left empty it keeps the field name. */}
|
||||
<Input
|
||||
unstyle
|
||||
className="mt-1.5 w-full rounded-md border border-slate-300 bg-white px-2 py-1.5 text-[10px] dark:border-slate-700 dark:bg-slate-900"
|
||||
className="mt-1.5 w-full rounded-md border border-slate-300 bg-white px-2 py-1.5 text-[10px] dark:border-gray-700 dark:bg-gray-900"
|
||||
placeholder={translate(
|
||||
'::App.DeveloperKitComponentDesigner.ColumnCaptionPlaceholder',
|
||||
{ column },
|
||||
|
|
@ -4062,7 +4062,7 @@ const VisualComponentDesigner = () => {
|
|||
const renderTabularColumnConfiguration = () => {
|
||||
if (!isTabularDataComponent(selectedNode?.type)) return null
|
||||
return (
|
||||
<div className="rounded-lg border border-slate-200 p-3 dark:border-slate-800">
|
||||
<div className="rounded-lg border border-slate-200 p-3 dark:border-gray-700">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<span className="text-[10px] font-semibold uppercase tracking-wider text-slate-400">
|
||||
{translate('::App.DeveloperKitComponentDesigner.VisibleColumns')}
|
||||
|
|
@ -4082,7 +4082,7 @@ const VisualComponentDesigner = () => {
|
|||
{gridColumnFields.map((field) => (
|
||||
<label
|
||||
key={field.path}
|
||||
className="flex cursor-pointer items-center gap-2 rounded bg-slate-50 px-2 py-1.5 dark:bg-slate-950"
|
||||
className="flex cursor-pointer items-center gap-2 rounded bg-slate-50 px-2 py-1.5 dark:bg-gray-950"
|
||||
>
|
||||
<Input
|
||||
unstyle
|
||||
|
|
@ -4099,7 +4099,7 @@ const VisualComponentDesigner = () => {
|
|||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="rounded bg-slate-50 p-2 text-[10px] leading-4 text-slate-500 dark:bg-slate-950">
|
||||
<p className="rounded bg-slate-50 p-2 text-[10px] leading-4 text-slate-500 dark:bg-gray-950">
|
||||
{translate('::App.DeveloperKitComponentDesigner.AddObjectForColumns')}
|
||||
</p>
|
||||
)}
|
||||
|
|
@ -4209,7 +4209,7 @@ const VisualComponentDesigner = () => {
|
|||
}))
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-slate-200 p-3 dark:border-slate-800">
|
||||
<div className="rounded-lg border border-slate-200 p-3 dark:border-gray-700">
|
||||
<div className="mb-1 flex items-center justify-between">
|
||||
<span className="text-[10px] font-semibold uppercase tracking-wider text-slate-400">
|
||||
{translate('::App.DeveloperKitComponentDesigner.Filters')}
|
||||
|
|
@ -4236,7 +4236,7 @@ const VisualComponentDesigner = () => {
|
|||
))}
|
||||
</datalist>
|
||||
{!filters.length && (
|
||||
<p className="rounded bg-slate-50 p-2 text-[10px] leading-4 text-slate-500 dark:bg-slate-950">
|
||||
<p className="rounded bg-slate-50 p-2 text-[10px] leading-4 text-slate-500 dark:bg-gray-950">
|
||||
{translate('::App.DeveloperKitComponentDesigner.NoFilter')}
|
||||
</p>
|
||||
)}
|
||||
|
|
@ -4257,7 +4257,7 @@ const VisualComponentDesigner = () => {
|
|||
return (
|
||||
<div
|
||||
key={filter.id}
|
||||
className="space-y-1.5 rounded-md border border-slate-200 bg-slate-50 p-2 dark:border-slate-800 dark:bg-slate-950"
|
||||
className="space-y-1.5 rounded-md border border-slate-200 bg-slate-50 p-2 dark:border-gray-700 dark:bg-gray-950"
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
{strictColumns ? (
|
||||
|
|
@ -4278,7 +4278,7 @@ const VisualComponentDesigner = () => {
|
|||
) : (
|
||||
<Input
|
||||
unstyle
|
||||
className="min-w-0 flex-1 rounded border border-slate-300 bg-white px-2 py-1 text-[10px] dark:border-slate-700 dark:bg-slate-900"
|
||||
className="min-w-0 flex-1 rounded border border-slate-300 bg-white px-2 py-1 text-[10px] dark:border-gray-700 dark:bg-gray-900"
|
||||
list={listId}
|
||||
placeholder={translate('::App.DeveloperKitComponentDesigner.FilterColumn')}
|
||||
value={filter.field || ''}
|
||||
|
|
@ -4334,7 +4334,7 @@ const VisualComponentDesigner = () => {
|
|||
{filter.source !== 'record' && (
|
||||
<Input
|
||||
unstyle
|
||||
className="min-w-0 flex-1 rounded border border-slate-300 bg-white px-2 py-1 text-[10px] dark:border-slate-700 dark:bg-slate-900"
|
||||
className="min-w-0 flex-1 rounded border border-slate-300 bg-white px-2 py-1 text-[10px] dark:border-gray-700 dark:bg-gray-900"
|
||||
placeholder={translate(
|
||||
filter.source === 'static'
|
||||
? '::App.DeveloperKitComponentDesigner.FilterValue'
|
||||
|
|
@ -4373,7 +4373,7 @@ const VisualComponentDesigner = () => {
|
|||
/>
|
||||
<Input
|
||||
unstyle
|
||||
className="min-w-0 flex-1 rounded border border-slate-300 bg-white px-2 py-1 text-[10px] dark:border-slate-700 dark:bg-slate-900"
|
||||
className="min-w-0 flex-1 rounded border border-slate-300 bg-white px-2 py-1 text-[10px] dark:border-gray-700 dark:bg-gray-900"
|
||||
list={`${listId}-${filter.id}-master`}
|
||||
placeholder={translate(
|
||||
'::App.DeveloperKitComponentDesigner.FilterMasterColumn',
|
||||
|
|
@ -4400,7 +4400,7 @@ const VisualComponentDesigner = () => {
|
|||
{!valueless && filter.source !== 'static' && (
|
||||
<Input
|
||||
unstyle
|
||||
className="w-full rounded border border-dashed border-slate-300 bg-white px-2 py-1 text-[10px] dark:border-slate-700 dark:bg-slate-900"
|
||||
className="w-full rounded border border-dashed border-slate-300 bg-white px-2 py-1 text-[10px] dark:border-gray-700 dark:bg-gray-900"
|
||||
placeholder={translate(
|
||||
'::App.DeveloperKitComponentDesigner.FilterPreviewValue',
|
||||
)}
|
||||
|
|
@ -4478,7 +4478,7 @@ const VisualComponentDesigner = () => {
|
|||
// close the permission menu the instant it opens.
|
||||
<div
|
||||
key={slot.property}
|
||||
className="block rounded-lg border border-slate-200 p-3 dark:border-slate-800"
|
||||
className="block rounded-lg border border-slate-200 p-3 dark:border-gray-700"
|
||||
>
|
||||
<span className="mb-1 flex items-center justify-between text-[10px] font-semibold uppercase tracking-wider text-slate-400">
|
||||
<span>{slot.label}</span>
|
||||
|
|
@ -4547,7 +4547,7 @@ const VisualComponentDesigner = () => {
|
|||
</span>
|
||||
<Input
|
||||
size="xs"
|
||||
className="w-full rounded-md border border-slate-300 bg-white px-2 py-2 text-[10px] dark:border-slate-700 dark:bg-slate-900"
|
||||
className="w-full rounded-md border border-slate-300 bg-white px-2 py-2 text-[10px] dark:border-gray-700 dark:bg-gray-900"
|
||||
placeholder="id"
|
||||
value={String(selectedNode.props.keyFieldName ?? '')}
|
||||
onChange={(event) => updateSelectedProp('keyFieldName', event.target.value)}
|
||||
|
|
@ -4576,7 +4576,7 @@ const VisualComponentDesigner = () => {
|
|||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="rounded-lg border border-slate-200 p-3 dark:border-slate-800">
|
||||
<div className="rounded-lg border border-slate-200 p-3 dark:border-gray-700">
|
||||
<div className="mb-1 text-[10px] font-semibold uppercase tracking-wider text-slate-400">
|
||||
Select key parametresi
|
||||
</div>
|
||||
|
|
@ -4594,7 +4594,7 @@ const VisualComponentDesigner = () => {
|
|||
/>
|
||||
<Input
|
||||
unstyle
|
||||
className="w-full rounded-md border border-slate-300 bg-white px-2 py-2 text-[10px] dark:border-slate-700 dark:bg-slate-900"
|
||||
className="w-full rounded-md border border-slate-300 bg-white px-2 py-2 text-[10px] dark:border-gray-700 dark:bg-gray-900"
|
||||
placeholder={translate('::App.DeveloperKitComponentDesigner.KeyParamPlaceholder', {
|
||||
field: getFormKeyField(selectedNode),
|
||||
})}
|
||||
|
|
@ -4604,7 +4604,7 @@ const VisualComponentDesigner = () => {
|
|||
</div>
|
||||
{renderDataSourceFilters(selectSource)}
|
||||
{selectNeedsPreviewKey && (
|
||||
<div className="rounded-lg border border-slate-200 p-3 dark:border-slate-800">
|
||||
<div className="rounded-lg border border-slate-200 p-3 dark:border-gray-700">
|
||||
<div className="mb-1 text-[10px] font-semibold uppercase tracking-wider text-slate-400">
|
||||
{translate('::App.DeveloperKitComponentDesigner.PreviewKeyValue')}
|
||||
</div>
|
||||
|
|
@ -4613,7 +4613,7 @@ const VisualComponentDesigner = () => {
|
|||
</p>
|
||||
<Input
|
||||
unstyle
|
||||
className="w-full rounded-md border border-slate-300 bg-white px-2 py-2 text-[10px] dark:border-slate-700 dark:bg-slate-900"
|
||||
className="w-full rounded-md border border-slate-300 bg-white px-2 py-2 text-[10px] dark:border-gray-700 dark:bg-gray-900"
|
||||
placeholder={translate(
|
||||
'::App.DeveloperKitComponentDesigner.PreviewKeyValuePlaceholder',
|
||||
)}
|
||||
|
|
@ -4622,7 +4622,7 @@ const VisualComponentDesigner = () => {
|
|||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-1.5 rounded-lg border border-slate-200 p-3 dark:border-slate-800">
|
||||
<div className="space-y-1.5 rounded-lg border border-slate-200 p-3 dark:border-gray-700">
|
||||
{(
|
||||
[
|
||||
['autoLoad', 'App.DeveloperKitComponentDesigner.AutoLoad', true],
|
||||
|
|
@ -4645,7 +4645,7 @@ const VisualComponentDesigner = () => {
|
|||
</label>
|
||||
))}
|
||||
</div>
|
||||
<div className="rounded-lg border border-slate-200 p-3 dark:border-slate-800">
|
||||
<div className="rounded-lg border border-slate-200 p-3 dark:border-gray-700">
|
||||
<div className="mb-2 text-[10px] font-semibold uppercase tracking-wider text-slate-400">
|
||||
{translate('::App.DeveloperKitComponentDesigner.RecordColumns')}
|
||||
</div>
|
||||
|
|
@ -4654,7 +4654,7 @@ const VisualComponentDesigner = () => {
|
|||
{recordFields.map((field) => (
|
||||
<div
|
||||
key={field.path}
|
||||
className="flex items-center justify-between gap-2 rounded bg-slate-50 px-2 py-1.5 dark:bg-slate-950"
|
||||
className="flex items-center justify-between gap-2 rounded bg-slate-50 px-2 py-1.5 dark:bg-gray-950"
|
||||
title={field.preview}
|
||||
>
|
||||
<code className="min-w-0 flex-1 truncate text-[10px] text-sky-700 dark:text-sky-300">
|
||||
|
|
@ -4762,7 +4762,7 @@ const VisualComponentDesigner = () => {
|
|||
: 'text'
|
||||
|
||||
return (
|
||||
<div className="space-y-3 rounded-lg border border-slate-200 p-3 dark:border-slate-800">
|
||||
<div className="space-y-3 rounded-lg border border-slate-200 p-3 dark:border-gray-700">
|
||||
<div>
|
||||
<div className="mb-1 text-[10px] font-semibold uppercase tracking-wider text-slate-400">
|
||||
{translate('::App.DeveloperKitComponentDesigner.RecordField')} · {sqlRecordProperty}
|
||||
|
|
@ -4772,7 +4772,7 @@ const VisualComponentDesigner = () => {
|
|||
</p>
|
||||
<Input
|
||||
unstyle
|
||||
className="w-full rounded-md border border-slate-300 bg-white px-2 py-2 text-[10px] dark:border-slate-700 dark:bg-slate-900"
|
||||
className="w-full rounded-md border border-slate-300 bg-white px-2 py-2 text-[10px] dark:border-gray-700 dark:bg-gray-900"
|
||||
list={columnListId}
|
||||
placeholder={translate('::App.DeveloperKitComponentDesigner.DoNotBindToRecord')}
|
||||
value={column}
|
||||
|
|
@ -4817,7 +4817,7 @@ const VisualComponentDesigner = () => {
|
|||
) : (
|
||||
<Input
|
||||
unstyle
|
||||
className="w-full rounded-md border border-slate-300 bg-white px-2 py-2 text-[10px] disabled:opacity-50 dark:border-slate-700 dark:bg-slate-900"
|
||||
className="w-full rounded-md border border-slate-300 bg-white px-2 py-2 text-[10px] disabled:opacity-50 dark:border-gray-700 dark:bg-gray-900"
|
||||
disabled={!column}
|
||||
placeholder={translate('::App.DeveloperKitComponentDesigner.NoDefaultValue')}
|
||||
type={defaultInputType}
|
||||
|
|
@ -4831,7 +4831,7 @@ const VisualComponentDesigner = () => {
|
|||
{SQL_DEFAULT_VALUE_TOKENS.map((token) => (
|
||||
<button
|
||||
key={token}
|
||||
className="rounded border border-slate-300 px-1.5 py-0.5 font-mono text-[9px] text-slate-600 hover:border-sky-400 hover:text-sky-700 disabled:opacity-40 dark:border-slate-700 dark:text-slate-300"
|
||||
className="rounded border border-slate-300 px-1.5 py-0.5 font-mono text-[9px] text-slate-600 hover:border-sky-400 hover:text-sky-700 disabled:opacity-40 dark:border-gray-700 dark:text-gray-300"
|
||||
disabled={!column || sqlRecordProperty === 'checked'}
|
||||
type="button"
|
||||
onClick={() => updateSelectedProp(SQL_DEFAULT_VALUE_PROP, `@${token}`)}
|
||||
|
|
@ -4887,7 +4887,7 @@ const VisualComponentDesigner = () => {
|
|||
<div className="space-y-3">
|
||||
{scopeNotice}
|
||||
{selectModeSelector}
|
||||
<div className="rounded-lg border border-dashed border-slate-300 p-3 text-xs leading-5 text-slate-500 dark:border-slate-700">
|
||||
<div className="rounded-lg border border-dashed border-slate-300 p-3 text-xs leading-5 text-slate-500 dark:border-gray-700">
|
||||
{translate('::App.DeveloperKitComponentDesigner.NoSelectableGetEndpoint')}
|
||||
</div>
|
||||
{renderSqlRecordField()}
|
||||
|
|
@ -4901,7 +4901,7 @@ const VisualComponentDesigner = () => {
|
|||
{selectModeSelector}
|
||||
{/* Inside a Form a record field has exactly one legal source, so
|
||||
the picker is replaced by a read-only statement of that source. */}
|
||||
<div className="rounded-lg border border-slate-200 p-3 dark:border-slate-800">
|
||||
<div className="rounded-lg border border-slate-200 p-3 dark:border-gray-700">
|
||||
<label className="block">
|
||||
<span className="mb-1.5 block text-[10px] font-semibold uppercase tracking-wider text-slate-400">
|
||||
{sqlScopeLocked
|
||||
|
|
@ -4911,7 +4911,7 @@ const VisualComponentDesigner = () => {
|
|||
: 'Endpoint'}
|
||||
</span>
|
||||
{sqlScopeLocked ? (
|
||||
<div className="rounded-md border border-slate-200 bg-slate-50 px-2.5 py-2 text-xs text-slate-600 dark:border-slate-700 dark:bg-slate-950 dark:text-slate-300">
|
||||
<div className="rounded-md border border-slate-200 bg-slate-50 px-2.5 py-2 text-xs text-slate-600 dark:border-gray-700 dark:bg-gray-950 dark:text-gray-300">
|
||||
{sqlScopeSource?.name}
|
||||
</div>
|
||||
) : (
|
||||
|
|
@ -4991,7 +4991,7 @@ const VisualComponentDesigner = () => {
|
|||
{isTabularDataComponent(selectedNode?.type) &&
|
||||
activeDataSample !== undefined &&
|
||||
tabularItemsBinding?.sourceId && (
|
||||
<div className="rounded-lg border border-slate-200 p-3 dark:border-slate-800">
|
||||
<div className="rounded-lg border border-slate-200 p-3 dark:border-gray-700">
|
||||
<label className="block">
|
||||
<span className="mb-1 block text-[10px] font-semibold uppercase tracking-wider text-slate-400">
|
||||
Koleksiyon
|
||||
|
|
@ -5022,7 +5022,7 @@ const VisualComponentDesigner = () => {
|
|||
)}
|
||||
|
||||
{!isOptionDataComponent(selectedNode?.type) && activeBindingSample !== undefined && (
|
||||
<div className="rounded-lg border border-slate-200 p-3 dark:border-slate-800">
|
||||
<div className="rounded-lg border border-slate-200 p-3 dark:border-gray-700">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<span className="text-[10px] font-semibold uppercase tracking-wider text-slate-400">
|
||||
{translate('::App.DeveloperKitComponentDesigner.AvailableFields')}
|
||||
|
|
@ -5069,7 +5069,7 @@ const VisualComponentDesigner = () => {
|
|||
activeDataFields.map((field) => (
|
||||
<label
|
||||
key={field.path}
|
||||
className="grid cursor-pointer grid-cols-[auto_1fr_auto] gap-2 rounded bg-slate-50 px-2 py-1.5 dark:bg-slate-950"
|
||||
className="grid cursor-pointer grid-cols-[auto_1fr_auto] gap-2 rounded bg-slate-50 px-2 py-1.5 dark:bg-gray-950"
|
||||
title={field.preview}
|
||||
>
|
||||
{isTabularDataComponent(selectedNode?.type) &&
|
||||
|
|
@ -5111,7 +5111,7 @@ const VisualComponentDesigner = () => {
|
|||
{translate('::App.DeveloperKitComponentDesigner.PropertyBindings')}
|
||||
</div>
|
||||
{!orderedBindableProperties.length ? (
|
||||
<p className="rounded-lg border border-dashed border-slate-300 p-3 text-xs leading-5 text-slate-500 dark:border-slate-700">
|
||||
<p className="rounded-lg border border-dashed border-slate-300 p-3 text-xs leading-5 text-slate-500 dark:border-gray-700">
|
||||
{translate('::App.DeveloperKitComponentDesigner.NoBindableProperty')}
|
||||
</p>
|
||||
) : (
|
||||
|
|
@ -5163,9 +5163,9 @@ const VisualComponentDesigner = () => {
|
|||
return (
|
||||
<label
|
||||
key={property.name}
|
||||
className="mb-2 block rounded-lg border border-slate-200 p-2 dark:border-slate-800"
|
||||
className="mb-2 block rounded-lg border border-slate-200 p-2 dark:border-gray-700"
|
||||
>
|
||||
<span className="mb-1.5 flex items-center justify-between text-[11px] font-medium text-slate-600 dark:text-slate-300">
|
||||
<span className="mb-1.5 flex items-center justify-between text-[11px] font-medium text-slate-600 dark:text-gray-300">
|
||||
<span>{property.name}</span>
|
||||
<span className="text-[9px] font-normal text-slate-400">
|
||||
{property.tsType || property.type}
|
||||
|
|
@ -5232,7 +5232,7 @@ const VisualComponentDesigner = () => {
|
|||
nodes.map((node) => (
|
||||
<React.Fragment key={node.id}>
|
||||
<button
|
||||
className={`flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-xs ${selectedId === node.id ? 'bg-sky-100 font-semibold text-sky-700 dark:bg-sky-950 dark:text-sky-300' : 'text-slate-600 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-800'}`}
|
||||
className={`flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-xs ${selectedId === node.id ? 'bg-sky-100 font-semibold text-sky-700 dark:bg-sky-950 dark:text-sky-300' : 'text-slate-600 hover:bg-slate-100 dark:text-gray-300 dark:hover:bg-gray-800'}`}
|
||||
style={{ paddingLeft: 8 + depth * 14 }}
|
||||
type="button"
|
||||
onClick={() => selectDesignerNode(node.id)}
|
||||
|
|
@ -5344,12 +5344,12 @@ const VisualComponentDesigner = () => {
|
|||
: ''
|
||||
|
||||
return (
|
||||
<div className="flex h-[calc(100vh-72px)] min-h-[720px] flex-col overflow-hidden rounded-xl border border-slate-200 bg-slate-100 shadow-sm dark:border-slate-800 dark:bg-slate-950">
|
||||
<div className="flex h-[calc(100vh-72px)] min-h-[720px] flex-col overflow-hidden rounded-xl border border-slate-200 bg-slate-100 shadow-sm dark:border-gray-700 dark:bg-gray-800">
|
||||
{pageHelmet}
|
||||
<header className="flex h-16 shrink-0 items-center justify-between border-b border-slate-200 bg-white px-4 dark:border-slate-800 dark:bg-slate-900">
|
||||
<header className="flex h-16 shrink-0 items-center justify-between border-b border-slate-200 bg-white px-4 dark:border-gray-700 dark:bg-gray-900">
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<Link
|
||||
className="rounded-md p-2 text-slate-500 hover:bg-slate-100 dark:hover:bg-slate-800"
|
||||
className="rounded-md p-2 text-slate-500 hover:bg-slate-100 dark:hover:bg-gray-800"
|
||||
title={translate('::App.DeveloperKitComponentEditor.Back')}
|
||||
to={ROUTES_ENUM.protected.saas.developerKit.components}
|
||||
>
|
||||
|
|
@ -5393,7 +5393,7 @@ const VisualComponentDesigner = () => {
|
|||
variant="plain"
|
||||
onClick={redo}
|
||||
/>
|
||||
<div className="mx-1 h-6 w-px bg-slate-200 dark:bg-slate-700" />
|
||||
<div className="mx-1 h-6 w-px bg-slate-200 dark:bg-gray-700" />
|
||||
<Button
|
||||
icon={<FaSync />}
|
||||
size="sm"
|
||||
|
|
@ -5435,8 +5435,8 @@ const VisualComponentDesigner = () => {
|
|||
|
||||
<div className="flex min-h-0 flex-1">
|
||||
{workspaceTab === 'design' && (
|
||||
<aside className="flex w-80 shrink-0 flex-col border-r border-slate-200 bg-white dark:border-slate-800 dark:bg-slate-900">
|
||||
<div className="border-b border-slate-200 p-3 dark:border-slate-800">
|
||||
<aside className="flex w-80 shrink-0 flex-col border-r border-slate-200 bg-white dark:border-gray-700 dark:bg-gray-900">
|
||||
<div className="border-b border-slate-200 p-3 dark:border-gray-700">
|
||||
<div className="mb-2 flex items-center gap-2 text-xs font-semibold uppercase tracking-wider text-slate-500">
|
||||
<FaPlus /> Toolbox
|
||||
</div>
|
||||
|
|
@ -5444,7 +5444,7 @@ const VisualComponentDesigner = () => {
|
|||
<FaSearch className="absolute left-2.5 top-2.5 text-xs text-slate-400" />
|
||||
<Input
|
||||
unstyle
|
||||
className="w-full rounded-md border border-slate-200 bg-slate-50 py-2 pl-8 pr-2 text-xs outline-none focus:border-sky-400 dark:border-slate-700 dark:bg-slate-950"
|
||||
className="w-full rounded-md border border-slate-200 bg-slate-50 py-2 pl-8 pr-2 text-xs outline-none focus:border-sky-400 dark:border-gray-700 dark:bg-gray-950"
|
||||
placeholder={translate('::App.DeveloperKitComponentDesigner.SearchComponent')}
|
||||
value={search}
|
||||
onChange={(event) => setSearch(event.target.value)}
|
||||
|
|
@ -5456,7 +5456,7 @@ const VisualComponentDesigner = () => {
|
|||
<section key={group} className="mb-5">
|
||||
<button
|
||||
aria-expanded={expandedToolboxGroups[group] || Boolean(search.trim())}
|
||||
className="mb-2 flex w-full items-center gap-2 rounded bg-slate-100 px-2 py-1.5 text-left text-[10px] font-bold uppercase tracking-widest text-slate-600 dark:bg-slate-800 dark:text-slate-300"
|
||||
className="mb-2 flex w-full items-center gap-2 rounded bg-slate-100 px-2 py-1.5 text-left text-[10px] font-bold uppercase tracking-widest text-slate-600 dark:bg-gray-800 dark:text-gray-300"
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setExpandedToolboxGroups((current) => ({
|
||||
|
|
@ -5471,7 +5471,7 @@ const VisualComponentDesigner = () => {
|
|||
<FaChevronRight className="shrink-0" />
|
||||
)}
|
||||
<span>{TOOLBOX_GROUP_LABELS[group]}</span>
|
||||
<span className="ml-auto rounded bg-white/70 px-1.5 py-0.5 text-[9px] font-medium tracking-normal dark:bg-slate-900/60">
|
||||
<span className="ml-auto rounded bg-white/70 px-1.5 py-0.5 text-[9px] font-medium tracking-normal dark:bg-gray-900/60">
|
||||
{definitions.length}
|
||||
</span>
|
||||
</button>
|
||||
|
|
@ -5481,10 +5481,10 @@ const VisualComponentDesigner = () => {
|
|||
<button
|
||||
key={`${definition.toolboxGroup}-${definition.name}`}
|
||||
draggable
|
||||
className={`group min-w-0 rounded-lg border bg-white px-2 py-2.5 text-left transition hover:border-sky-400 hover:bg-sky-50 dark:bg-slate-900 dark:hover:border-sky-600 dark:hover:bg-sky-950 ${
|
||||
className={`group min-w-0 rounded-lg border bg-white px-2 py-2.5 text-left transition hover:border-sky-400 hover:bg-sky-50 dark:bg-gray-900 dark:hover:border-sky-600 dark:hover:bg-sky-950 ${
|
||||
activeDrag?.source === 'library' && activeDrag.name === definition.name
|
||||
? 'border-sky-500 ring-2 ring-sky-300 dark:border-sky-500'
|
||||
: 'border-slate-200 dark:border-slate-700'
|
||||
: 'border-slate-200 dark:border-gray-700'
|
||||
}`}
|
||||
title={
|
||||
definition.acceptsChildren
|
||||
|
|
@ -5509,10 +5509,10 @@ const VisualComponentDesigner = () => {
|
|||
}}
|
||||
onDragEnd={() => endDesignerDrag()}
|
||||
>
|
||||
<div className="mb-1 flex h-6 w-6 items-center justify-center rounded bg-slate-100 text-[10px] font-bold text-slate-500 group-hover:bg-sky-100 group-hover:text-sky-600 dark:bg-slate-800">
|
||||
<div className="mb-1 flex h-6 w-6 items-center justify-center rounded bg-slate-100 text-[10px] font-bold text-slate-500 group-hover:bg-sky-100 group-hover:text-sky-600 dark:bg-gray-800">
|
||||
{definition.name.slice(0, 2).toUpperCase()}
|
||||
</div>
|
||||
<div className="truncate text-[11px] font-medium text-slate-700 dark:text-slate-200">
|
||||
<div className="truncate text-[11px] font-medium text-slate-700 dark:text-gray-200">
|
||||
{definition.name}
|
||||
</div>
|
||||
</button>
|
||||
|
|
@ -5526,7 +5526,7 @@ const VisualComponentDesigner = () => {
|
|||
)}
|
||||
|
||||
<main className="flex min-w-0 flex-1 flex-col">
|
||||
<div className="flex h-12 shrink-0 items-center justify-between border-b border-slate-200 bg-white px-3 dark:border-slate-800 dark:bg-slate-900">
|
||||
<div className="flex h-12 shrink-0 items-center justify-between border-b border-slate-200 bg-white px-3 dark:border-gray-700 dark:bg-gray-900">
|
||||
<div className="flex h-full items-center gap-1">
|
||||
{(
|
||||
[
|
||||
|
|
@ -5549,7 +5549,7 @@ const VisualComponentDesigner = () => {
|
|||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{workspaceTab === 'design' && (
|
||||
<div className="flex items-center rounded-md border border-slate-200 p-0.5 dark:border-slate-700">
|
||||
<div className="flex items-center rounded-md border border-slate-200 p-0.5 dark:border-gray-700">
|
||||
{(
|
||||
[
|
||||
['responsive', <FaDesktop key="r" />, 'Responsive'],
|
||||
|
|
@ -5560,7 +5560,7 @@ const VisualComponentDesigner = () => {
|
|||
).map(([width, icon, label]) => (
|
||||
<button
|
||||
key={width}
|
||||
className={`rounded p-1.5 ${document.canvas.width === width ? 'bg-sky-100 text-sky-600 dark:bg-sky-950' : 'text-slate-400 hover:text-slate-700 dark:hover:text-slate-200'}`}
|
||||
className={`rounded p-1.5 ${document.canvas.width === width ? 'bg-sky-100 text-sky-600 dark:bg-sky-950' : 'text-slate-400 hover:text-slate-700 dark:hover:text-gray-200'}`}
|
||||
title={label}
|
||||
type="button"
|
||||
onClick={() =>
|
||||
|
|
@ -5581,7 +5581,7 @@ const VisualComponentDesigner = () => {
|
|||
className={`flex items-center gap-1.5 rounded-md border px-2.5 py-1.5 text-xs font-medium ${
|
||||
scriptApiOpen
|
||||
? 'border-sky-500 bg-sky-50 text-sky-600 dark:bg-sky-950'
|
||||
: 'border-slate-200 text-slate-500 hover:text-slate-800 dark:border-slate-700 dark:hover:text-white'
|
||||
: 'border-slate-200 text-slate-500 hover:text-slate-800 dark:border-gray-700 dark:hover:text-white'
|
||||
}`}
|
||||
title={translate('::App.DeveloperKitComponentDesigner.ScriptApiExamples')}
|
||||
type="button"
|
||||
|
|
@ -5596,28 +5596,24 @@ const VisualComponentDesigner = () => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className="min-h-0 flex-1 overflow-auto bg-slate-200 p-5 dark:bg-slate-950">
|
||||
<div className="min-h-0 flex-1 overflow-auto bg-slate-200 p-5 dark:bg-gray-950">
|
||||
{workspaceTab === 'design' ? (
|
||||
<div
|
||||
className="mx-auto min-h-full overflow-hidden rounded-lg bg-white text-slate-900 shadow-lg transition-[width] duration-200 dark:bg-slate-900 dark:text-slate-100"
|
||||
className="mx-auto min-h-full overflow-hidden rounded-lg bg-white text-slate-900 shadow-lg transition-[width] duration-200 dark:bg-gray-900 dark:text-gray-100"
|
||||
style={{ width: CANVAS_WIDTHS[document.canvas.width], maxWidth: '100%' }}
|
||||
>
|
||||
{canvas}
|
||||
</div>
|
||||
) : workspaceTab === 'data' ? (
|
||||
<div className="w-full space-y-4">
|
||||
<div className="flex items-center justify-between rounded-lg border border-slate-200 bg-white p-4 shadow-sm dark:border-slate-800 dark:bg-slate-900">
|
||||
<div className="flex items-center justify-between rounded-lg border border-slate-200 bg-white p-4 shadow-sm dark:border-gray-700 dark:bg-gray-900">
|
||||
<div className="min-w-0">
|
||||
<h2 className="flex items-center gap-2 font-semibold text-slate-800 dark:text-white">
|
||||
<FaDatabase className="text-sky-600" /> Data sources
|
||||
</h2>
|
||||
<p className="mt-1 text-xs text-slate-500">
|
||||
{translate('::App.DeveloperKitComponentDesigner.ApiAuthHint')}
|
||||
</p>
|
||||
<p className="rounded-lg border border-sky-200 bg-sky-50 p-3 text-xs text-sky-900 dark:border-sky-900 dark:bg-sky-950 dark:text-sky-200">
|
||||
{translate('::App.DeveloperKitComponentDesigner.GetPostHint')}
|
||||
</p>
|
||||
<p className="rounded-lg border border-sky-200 bg-sky-50 p-3 text-xs text-sky-900 dark:border-sky-900 dark:bg-sky-950 dark:text-sky-200">
|
||||
{translate('::App.DeveloperKitComponentDesigner.ApiAuthHint')}{' '}
|
||||
{translate('::App.DeveloperKitComponentDesigner.GetPostHint')}{' '}
|
||||
{translate('::App.DeveloperKitComponentDesigner.EndpointSettingsHint')}
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -5628,7 +5624,7 @@ const VisualComponentDesigner = () => {
|
|||
</div>
|
||||
</div>
|
||||
{(dataSourceCatalog.length > 0 || endpointCatalogError) && (
|
||||
<div className="rounded-lg border border-slate-200 bg-white p-4 shadow-sm dark:border-slate-800 dark:bg-slate-900">
|
||||
<div className="rounded-lg border border-slate-200 bg-white p-4 shadow-sm dark:border-gray-700 dark:bg-gray-900">
|
||||
<div className="mb-3">
|
||||
<h3 className="text-sm font-semibold text-slate-800 dark:text-white">
|
||||
{translate('::App.DeveloperKitComponentDesigner.AvailableSources')}
|
||||
|
|
@ -5669,8 +5665,8 @@ const VisualComponentDesigner = () => {
|
|||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex min-h-[620px] w-full overflow-hidden rounded-lg border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900">
|
||||
<div className="w-72 shrink-0 border-r border-slate-200 bg-slate-50 p-3 dark:border-slate-800 dark:bg-slate-950">
|
||||
<div className="flex min-h-[620px] w-full overflow-hidden rounded-lg border border-slate-200 bg-white shadow-sm dark:border-gray-700 dark:bg-gray-900">
|
||||
<div className="w-72 shrink-0 border-r border-slate-200 bg-slate-50 p-3 dark:border-gray-700 dark:bg-gray-950">
|
||||
<div className="mb-3">
|
||||
<h2 className="flex items-center gap-2 font-semibold text-slate-800 dark:text-white">
|
||||
<FaBolt className="text-amber-500" /> Event workspace
|
||||
|
|
@ -5684,7 +5680,7 @@ const VisualComponentDesigner = () => {
|
|||
javascriptEventTargets.map((target) => (
|
||||
<button
|
||||
key={target.node.id}
|
||||
className={`flex w-full items-center gap-2 rounded-md border px-2.5 py-2 text-left transition ${activeJavaScriptTarget?.node.id === target.node.id ? 'border-amber-400 bg-amber-50 text-amber-800 dark:bg-amber-950 dark:text-amber-200' : 'border-transparent text-slate-600 hover:border-slate-200 hover:bg-white dark:text-slate-300 dark:hover:border-slate-700 dark:hover:bg-slate-900'}`}
|
||||
className={`flex w-full items-center gap-2 rounded-md border px-2.5 py-2 text-left transition ${activeJavaScriptTarget?.node.id === target.node.id ? 'border-amber-400 bg-amber-50 text-amber-800 dark:bg-amber-950 dark:text-amber-200' : 'border-transparent text-slate-600 hover:border-slate-200 hover:bg-white dark:text-gray-300 dark:hover:border-gray-700 dark:hover:bg-gray-900'}`}
|
||||
type="button"
|
||||
onClick={() => setSelectedId(target.node.id)}
|
||||
>
|
||||
|
|
@ -5692,20 +5688,20 @@ const VisualComponentDesigner = () => {
|
|||
<span className="min-w-0 flex-1 truncate text-xs font-semibold">
|
||||
{target.node.type}
|
||||
</span>
|
||||
<span className="rounded bg-slate-200 px-1.5 py-0.5 text-[9px] dark:bg-slate-800">
|
||||
<span className="rounded bg-slate-200 px-1.5 py-0.5 text-[9px] dark:bg-gray-800">
|
||||
{target.eventNames.length}
|
||||
</span>
|
||||
</button>
|
||||
))
|
||||
) : (
|
||||
<p className="rounded-md border border-dashed border-slate-300 p-3 text-xs leading-5 text-slate-500 dark:border-slate-700">
|
||||
<p className="rounded-md border border-dashed border-slate-300 p-3 text-xs leading-5 text-slate-500 dark:border-gray-700">
|
||||
{translate('::App.DeveloperKitComponentDesigner.NoEventComponent')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{/* Every component on the page is addressable from any script,
|
||||
which is what makes cross component behaviour possible. */}
|
||||
<div className="mt-4 border-t border-slate-200 pt-3 dark:border-slate-800">
|
||||
<div className="mt-4 border-t border-slate-200 pt-3 dark:border-gray-700">
|
||||
<h3 className="text-[11px] font-semibold uppercase tracking-wider text-slate-400">
|
||||
Referanslar
|
||||
</h3>
|
||||
|
|
@ -5719,7 +5715,7 @@ const VisualComponentDesigner = () => {
|
|||
{[...documentRefs.entries()].map(([ref, node]) => (
|
||||
<button
|
||||
key={ref}
|
||||
className="flex w-full items-center gap-2 rounded border border-transparent px-2 py-1 text-left hover:border-slate-200 hover:bg-white dark:hover:border-slate-700 dark:hover:bg-slate-900"
|
||||
className="flex w-full items-center gap-2 rounded border border-transparent px-2 py-1 text-left hover:border-slate-200 hover:bg-white dark:hover:border-gray-700 dark:hover:bg-gray-900"
|
||||
title={translate('::App.DeveloperKitComponentDesigner.CopyRefName')}
|
||||
type="button"
|
||||
onClick={() => copyRefName(ref)}
|
||||
|
|
@ -5740,7 +5736,7 @@ const VisualComponentDesigner = () => {
|
|||
</div>
|
||||
|
||||
<div className="min-w-0 flex-1 overflow-y-auto p-5">
|
||||
<div className="mb-5 rounded-lg border border-slate-200 p-4 dark:border-slate-800">
|
||||
<div className="mb-5 rounded-lg border border-slate-200 p-4 dark:border-gray-700">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-slate-800 dark:text-white">
|
||||
|
|
@ -5791,10 +5787,10 @@ const VisualComponentDesigner = () => {
|
|||
{activeJavaScriptTarget.eventNames.map((eventName) => (
|
||||
<label
|
||||
key={eventName}
|
||||
className={`overflow-hidden rounded-lg border dark:border-slate-800 ${eventName === activeEventName ? 'border-amber-400' : 'border-slate-200'}`}
|
||||
className={`overflow-hidden rounded-lg border dark:border-gray-700 ${eventName === activeEventName ? 'border-amber-400' : 'border-slate-200'}`}
|
||||
onFocus={() => setFocusedEventName(eventName)}
|
||||
>
|
||||
<span className="flex items-center justify-between bg-slate-50 px-3 py-2 text-xs font-semibold text-slate-700 dark:bg-slate-950 dark:text-slate-200">
|
||||
<span className="flex items-center justify-between bg-slate-50 px-3 py-2 text-xs font-semibold text-slate-700 dark:bg-gray-950 dark:text-gray-200">
|
||||
<span
|
||||
className="min-w-0 truncate"
|
||||
title={
|
||||
|
|
@ -5877,7 +5873,7 @@ const VisualComponentDesigner = () => {
|
|||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-lg border border-dashed border-slate-300 p-8 text-center text-sm text-slate-500 dark:border-slate-700">
|
||||
<div className="rounded-lg border border-dashed border-slate-300 p-8 text-center text-sm text-slate-500 dark:border-gray-700">
|
||||
{translate('::App.DeveloperKitComponentDesigner.NoEditableEvent')}
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -5890,9 +5886,9 @@ const VisualComponentDesigner = () => {
|
|||
{workspaceTab === 'design' && (
|
||||
<aside
|
||||
data-property-panel
|
||||
className="flex w-72 shrink-0 flex-col border-l border-slate-200 bg-white dark:border-slate-800 dark:bg-slate-900"
|
||||
className="flex w-72 shrink-0 flex-col border-l border-slate-200 bg-white dark:border-gray-700 dark:bg-gray-900"
|
||||
>
|
||||
<div className="flex h-12 shrink-0 border-b border-slate-200 dark:border-slate-800">
|
||||
<div className="flex h-12 shrink-0 border-b border-slate-200 dark:border-gray-700">
|
||||
{(
|
||||
[
|
||||
['properties', <FaSlidersH key="p" />, 'Properties'],
|
||||
|
|
@ -5902,7 +5898,7 @@ const VisualComponentDesigner = () => {
|
|||
).map(([tab, icon, label]) => (
|
||||
<button
|
||||
key={tab}
|
||||
className={`flex flex-1 items-center justify-center gap-1 border-b-2 text-[11px] ${tab === 'data' && !selectedIsDataComponent ? 'cursor-not-allowed border-transparent text-slate-300 dark:text-slate-700' : inspectorTab === tab ? 'border-sky-500 text-sky-600' : 'border-transparent text-slate-500'}`}
|
||||
className={`flex flex-1 items-center justify-center gap-1 border-b-2 text-[11px] ${tab === 'data' && !selectedIsDataComponent ? 'cursor-not-allowed border-transparent text-slate-300 dark:text-gray-700' : inspectorTab === tab ? 'border-sky-500 text-sky-600' : 'border-transparent text-slate-500'}`}
|
||||
disabled={tab === 'data' && !selectedIsDataComponent}
|
||||
title={
|
||||
tab === 'data' && !selectedIsDataComponent
|
||||
|
|
@ -5932,7 +5928,7 @@ const VisualComponentDesigner = () => {
|
|||
</div>
|
||||
) : selectedNode ? (
|
||||
<>
|
||||
<div className="mb-4 rounded-lg bg-slate-50 p-3 dark:bg-slate-950">
|
||||
<div className="mb-4 rounded-lg bg-slate-50 p-3 dark:bg-gray-950">
|
||||
<div className="text-[10px] uppercase tracking-wider text-slate-400">
|
||||
Selected component
|
||||
</div>
|
||||
|
|
@ -5950,7 +5946,7 @@ const VisualComponentDesigner = () => {
|
|||
</button>
|
||||
</div>
|
||||
<code className="text-[10px] text-slate-400">{selectedNode.id}</code>
|
||||
<div className="mt-2 border-t border-slate-200 pt-2 dark:border-slate-800">
|
||||
<div className="mt-2 border-t border-slate-200 pt-2 dark:border-gray-700">
|
||||
<span className="flex items-center justify-between text-[10px] font-semibold uppercase tracking-wider text-slate-400">
|
||||
Ref
|
||||
<button
|
||||
|
|
@ -5964,7 +5960,7 @@ const VisualComponentDesigner = () => {
|
|||
</span>
|
||||
<Input
|
||||
unstyle
|
||||
className="mt-1 w-full rounded border border-slate-300 bg-white px-2 py-1 font-mono text-[11px] text-slate-700 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-200"
|
||||
className="mt-1 w-full rounded border border-slate-300 bg-white px-2 py-1 font-mono text-[11px] text-slate-700 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-200"
|
||||
spellCheck={false}
|
||||
value={refDraft}
|
||||
onBlur={commitRefRename}
|
||||
|
|
@ -5982,7 +5978,7 @@ const VisualComponentDesigner = () => {
|
|||
)}
|
||||
</div>
|
||||
{selectedDefinition?.interfaceName && (
|
||||
<div className="mt-2 border-t border-slate-200 pt-2 dark:border-slate-800">
|
||||
<div className="mt-2 border-t border-slate-200 pt-2 dark:border-gray-700">
|
||||
<div className="text-[10px] font-semibold text-sky-600">
|
||||
{selectedDefinition.interfaceName}
|
||||
</div>
|
||||
|
|
@ -6040,9 +6036,9 @@ const VisualComponentDesigner = () => {
|
|||
return (
|
||||
<div
|
||||
key={property.name}
|
||||
className="group/property mb-4 rounded-md border border-slate-200 p-2 dark:border-slate-800"
|
||||
className="group/property mb-4 rounded-md border border-slate-200 p-2 dark:border-gray-700"
|
||||
>
|
||||
<span className="mb-1.5 flex items-center justify-between text-[11px] font-medium text-slate-600 dark:text-slate-300">
|
||||
<span className="mb-1.5 flex items-center justify-between text-[11px] font-medium text-slate-600 dark:text-gray-300">
|
||||
<span>
|
||||
{property.name}
|
||||
{property.required ? (
|
||||
|
|
@ -6064,7 +6060,7 @@ const VisualComponentDesigner = () => {
|
|||
>
|
||||
<FaBolt />
|
||||
</button>
|
||||
<span className="pointer-events-none absolute right-0 top-full z-20 mt-1 hidden w-56 rounded-md border border-slate-200 bg-white p-2 text-left font-mono text-[9px] leading-4 text-sky-700 shadow-lg group-hover/snippet:block dark:border-slate-700 dark:bg-slate-950 dark:text-sky-300">
|
||||
<span className="pointer-events-none absolute right-0 top-full z-20 mt-1 hidden w-56 rounded-md border border-slate-200 bg-white p-2 text-left font-mono text-[9px] leading-4 text-sky-700 shadow-lg group-hover/snippet:block dark:border-gray-700 dark:bg-gray-950 dark:text-sky-300">
|
||||
{snippet}
|
||||
<span className="mt-1 block font-sans text-[9px] text-slate-400">
|
||||
{translate(
|
||||
|
|
@ -6108,7 +6104,7 @@ const VisualComponentDesigner = () => {
|
|||
</>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-lg border border-dashed border-slate-300 p-5 text-center dark:border-slate-700">
|
||||
<div className="rounded-lg border border-dashed border-slate-300 p-5 text-center dark:border-gray-700">
|
||||
<FaMousePointer className="mx-auto mb-2 text-slate-400" />
|
||||
<p className="text-xs text-slate-500">
|
||||
{translate('::App.DeveloperKitComponentDesigner.SelectComponentForProps')}
|
||||
|
|
@ -6117,7 +6113,7 @@ const VisualComponentDesigner = () => {
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="border-t border-slate-200 bg-slate-50 px-3 py-2 text-[10px] text-slate-500 dark:border-slate-800 dark:bg-slate-950">
|
||||
<div className="border-t border-slate-200 bg-slate-50 px-3 py-2 text-[10px] text-slate-500 dark:border-gray-700 dark:bg-gray-950">
|
||||
<span className="font-semibold">{document.nodes.length}</span> root component ·{' '}
|
||||
<span className="font-semibold">Ctrl+S</span> save ·{' '}
|
||||
<span className="font-semibold">Del</span> delete
|
||||
|
|
@ -6131,8 +6127,8 @@ const VisualComponentDesigner = () => {
|
|||
than a dialog on purpose: no backdrop, so the editor underneath stays
|
||||
usable while an example is being read or pasted. */}
|
||||
{scriptApiOpen && (
|
||||
<aside className="fixed inset-y-0 right-0 z-[90] flex w-full max-w-md flex-col border-l border-slate-200 bg-white shadow-2xl dark:border-slate-700 dark:bg-slate-900">
|
||||
<div className="flex shrink-0 items-start justify-between gap-3 border-b border-slate-200 px-4 py-3 dark:border-slate-800">
|
||||
<aside className="fixed inset-y-0 right-0 z-[90] flex w-full max-w-md flex-col border-l border-slate-200 bg-white shadow-2xl dark:border-gray-700 dark:bg-gray-900">
|
||||
<div className="flex shrink-0 items-start justify-between gap-3 border-b border-slate-200 px-4 py-3 dark:border-gray-700">
|
||||
<div className="min-w-0">
|
||||
<h2 className="flex items-center gap-2 text-sm font-semibold text-slate-800 dark:text-white">
|
||||
<FaBolt className="text-amber-500" /> Script API
|
||||
|
|
@ -6147,7 +6143,7 @@ const VisualComponentDesigner = () => {
|
|||
</code>
|
||||
</div>
|
||||
<button
|
||||
className="rounded p-1.5 text-slate-400 hover:bg-slate-100 hover:text-slate-700 dark:hover:bg-slate-800 dark:hover:text-white"
|
||||
className="rounded p-1.5 text-slate-400 hover:bg-slate-100 hover:text-slate-700 dark:hover:bg-gray-800 dark:hover:text-white"
|
||||
title={translate('::App.Platform.CloseWithEsc')}
|
||||
type="button"
|
||||
onClick={() => setScriptApiOpen(false)}
|
||||
|
|
@ -6155,10 +6151,10 @@ const VisualComponentDesigner = () => {
|
|||
×
|
||||
</button>
|
||||
</div>
|
||||
<div className="shrink-0 border-b border-slate-200 px-4 py-3 dark:border-slate-800">
|
||||
<div className="shrink-0 border-b border-slate-200 px-4 py-3 dark:border-gray-700">
|
||||
<Input
|
||||
unstyle
|
||||
className="w-full rounded-md border border-slate-300 bg-white px-3 py-1.5 text-xs text-slate-700 dark:border-slate-700 dark:bg-slate-950 dark:text-slate-200"
|
||||
className="w-full rounded-md border border-slate-300 bg-white px-3 py-1.5 text-xs text-slate-700 dark:border-gray-700 dark:bg-gray-950 dark:text-gray-200"
|
||||
placeholder={translate('::App.DeveloperKitComponentDesigner.SearchExample')}
|
||||
value={scriptApiSearch}
|
||||
onChange={(event) => setScriptApiSearch(event.target.value)}
|
||||
|
|
@ -6190,11 +6186,11 @@ const VisualComponentDesigner = () => {
|
|||
return (
|
||||
<div
|
||||
key={recipe.title}
|
||||
className="overflow-hidden rounded-md border border-slate-200 dark:border-slate-800"
|
||||
className="overflow-hidden rounded-md border border-slate-200 dark:border-gray-700"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2 px-3 pt-2">
|
||||
<div className="min-w-0">
|
||||
<div className="text-xs font-semibold text-slate-700 dark:text-slate-200">
|
||||
<div className="text-xs font-semibold text-slate-700 dark:text-gray-200">
|
||||
{translate('::' + recipe.title)}
|
||||
</div>
|
||||
<p className="text-[10px] leading-4 text-slate-400">
|
||||
|
|
@ -6240,7 +6236,7 @@ const VisualComponentDesigner = () => {
|
|||
)
|
||||
})}
|
||||
{!filteredScriptRecipes.length && (
|
||||
<p className="rounded-md border border-dashed border-slate-300 p-4 text-center text-xs text-slate-500 dark:border-slate-700">
|
||||
<p className="rounded-md border border-dashed border-slate-300 p-4 text-center text-xs text-slate-500 dark:border-gray-700">
|
||||
{translate('::App.DeveloperKitComponentDesigner.NoMatchingExample')}
|
||||
</p>
|
||||
)}
|
||||
|
|
@ -6250,8 +6246,8 @@ const VisualComponentDesigner = () => {
|
|||
|
||||
{catalogSourceEditor && (
|
||||
<div className="fixed inset-0 z-[100] flex items-center justify-center bg-slate-950/70 p-4">
|
||||
<div className="w-full max-w-2xl overflow-hidden rounded-xl border border-slate-200 bg-white shadow-2xl dark:border-slate-700 dark:bg-slate-900">
|
||||
<div className="flex items-start justify-between border-b border-slate-200 px-5 py-4 dark:border-slate-700">
|
||||
<div className="w-full max-w-2xl overflow-hidden rounded-xl border border-slate-200 bg-white shadow-2xl dark:border-gray-700 dark:bg-gray-900">
|
||||
<div className="flex items-start justify-between border-b border-slate-200 px-5 py-4 dark:border-gray-700">
|
||||
<div>
|
||||
<h2 className="text-sm font-semibold text-slate-800 dark:text-white">
|
||||
{translate(
|
||||
|
|
@ -6266,7 +6262,7 @@ const VisualComponentDesigner = () => {
|
|||
</p>
|
||||
</div>
|
||||
<button
|
||||
className="rounded p-1.5 text-slate-400 hover:bg-slate-100 hover:text-slate-700 dark:hover:bg-slate-800 dark:hover:text-white"
|
||||
className="rounded p-1.5 text-slate-400 hover:bg-slate-100 hover:text-slate-700 dark:hover:bg-gray-800 dark:hover:text-white"
|
||||
title={translate('::App.Platform.Close')}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
|
|
@ -6286,7 +6282,7 @@ const VisualComponentDesigner = () => {
|
|||
</span>
|
||||
<Input
|
||||
unstyle
|
||||
className="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-xs text-slate-800 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-950 dark:text-slate-100"
|
||||
className="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-xs text-slate-800 outline-none focus:border-sky-500 dark:border-gray-700 dark:bg-gray-950 dark:text-gray-100"
|
||||
value={catalogSourceEditor.draft.name}
|
||||
onChange={(event) => updateCatalogSourceDraft({ name: event.target.value })}
|
||||
/>
|
||||
|
|
@ -6323,7 +6319,7 @@ const VisualComponentDesigner = () => {
|
|||
</span>
|
||||
<Input
|
||||
unstyle
|
||||
className="w-full rounded-md border border-slate-300 bg-white px-3 py-2 font-mono text-xs text-slate-800 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-950 dark:text-slate-100"
|
||||
className="w-full rounded-md border border-slate-300 bg-white px-3 py-2 font-mono text-xs text-slate-800 outline-none focus:border-sky-500 dark:border-gray-700 dark:bg-gray-950 dark:text-gray-100"
|
||||
placeholder="/api/app/customers"
|
||||
value={catalogSourceEditor.draft.url}
|
||||
onChange={(event) => updateCatalogSourceDraft({ url: event.target.value })}
|
||||
|
|
@ -6338,7 +6334,7 @@ const VisualComponentDesigner = () => {
|
|||
</span>
|
||||
<Input
|
||||
unstyle
|
||||
className="w-full rounded-md border border-slate-300 bg-white px-3 py-2 font-mono text-xs text-slate-800 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-950 dark:text-slate-100"
|
||||
className="w-full rounded-md border border-slate-300 bg-white px-3 py-2 font-mono text-xs text-slate-800 outline-none focus:border-sky-500 dark:border-gray-700 dark:bg-gray-950 dark:text-gray-100"
|
||||
placeholder="result.items"
|
||||
value={catalogSourceEditor.draft.responsePath}
|
||||
onChange={(event) =>
|
||||
|
|
@ -6362,7 +6358,7 @@ const VisualComponentDesigner = () => {
|
|||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2 border-t border-slate-200 bg-slate-50 px-5 py-4 dark:border-slate-700 dark:bg-slate-950">
|
||||
<div className="flex justify-end gap-2 border-t border-slate-200 bg-slate-50 px-5 py-4 dark:border-gray-700 dark:bg-gray-950">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="plain"
|
||||
|
|
|
|||
|
|
@ -567,6 +567,37 @@ function hideDialogLater() {
|
|||
}, NOTICE_VISIBLE_MS)
|
||||
}
|
||||
|
||||
/**
|
||||
* Kurulum oturumu sürüyor mu? Isıtma mesajlarının araya girmesini engeller
|
||||
* (bkz. `handleWorkerMessage`). Geçişli kurulumda sayfa yenilendiği için
|
||||
* sıfırlanması yalnızca ilk kurulum (geçişsiz) akışında gerekir.
|
||||
*/
|
||||
let installSessionActive = false
|
||||
|
||||
/** Ekranda gösterilen oturumun fazı; faz değişince sayaçlar sıfırlanır. */
|
||||
let activePhase: 'install' | 'warmup' | undefined
|
||||
|
||||
/**
|
||||
* Oturumun türü BAŞLARKEN dondurulur.
|
||||
*
|
||||
* Başlık ve kapatılabilirlik her ilerleme mesajında yeniden hesaplanırsa araya
|
||||
* giren tek bir mesaj bile pencereyi "Downloading assets" ile "Downloading the
|
||||
* new version" arasında zıplatır. Karar oturum başına bir kez verilir.
|
||||
*/
|
||||
let sessionTransitions = false
|
||||
|
||||
/**
|
||||
* Oturum değiştiğinde birikmiş liste/bayt bilgisi taşınmaz: iki oturumun dosya
|
||||
* index'leri ayrı uzaylardadır, birleştirilirse satırlar birbirinin üzerine
|
||||
* yazılır ve ilerleme yanlış hesaplanır.
|
||||
*/
|
||||
function startSession(phase: 'install' | 'warmup' | undefined) {
|
||||
if (activePhase === phase) return
|
||||
activePhase = phase
|
||||
resetRate()
|
||||
setState({ files: [], loadedBytes: 0, totalBytes: 0, percent: 0, secondsRemaining: undefined })
|
||||
}
|
||||
|
||||
function handleWorkerMessage(event: MessageEvent) {
|
||||
const data = event.data as SwProgressMessage | undefined
|
||||
if (!data?.type) return
|
||||
|
|
@ -586,11 +617,20 @@ function handleWorkerMessage(event: MessageEvent) {
|
|||
*/
|
||||
const transitions = controlled && data.phase !== 'warmup'
|
||||
|
||||
// Isıtma kurulumla aynı anda koşabiliyor (eski aktif worker ısıtırken yenisi
|
||||
// kuruluyor). İki oturumun dosya listeleri ayrı index uzayında olduğu için
|
||||
// birleşince satırlar tekrarlanıyor ve toplam boyut aşıyordu; geçiş sürecinde
|
||||
// ısıtma mesajları yok sayılır.
|
||||
if (data.phase === 'warmup' && (activating || state.status === 'ready')) return
|
||||
/**
|
||||
* Kurulum başladığı anda ısıtma mesajları tamamen susturulur.
|
||||
*
|
||||
* Deploy sonrası iki oturum GERÇEKTEN aynı anda koşuyor: eski aktif worker
|
||||
* `ensureWarmup` ile eksik bileşenleri indirirken yeni worker kuruluyor. İki
|
||||
* oturumun dosya listeleri ayrı index uzayında olduğu için birleşince satırlar
|
||||
* birbirinin üzerine yazılıyor, toplam boyut şişiyor ve pencere her tick'te
|
||||
* "Downloading assets" ile "Downloading the new version" arasında gidip
|
||||
* geliyordu. Kurulum kazanır: çalışan sürümü değiştiren tek oturum odur.
|
||||
*/
|
||||
if (data.phase !== 'warmup') installSessionActive = true
|
||||
if (data.phase === 'warmup' && (installSessionActive || activating || state.status === 'ready')) {
|
||||
return
|
||||
}
|
||||
|
||||
const totals = { loadedBytes: data.loadedBytes, totalBytes: data.totalBytes }
|
||||
|
||||
|
|
@ -604,37 +644,50 @@ function handleWorkerMessage(event: MessageEvent) {
|
|||
// Geçişli akış kapatılamaz; önceki kapatma tercihi burada geçersizleşir.
|
||||
if (transitions) forceDialog()
|
||||
void showUpdateDialog('downloading')
|
||||
resetRate()
|
||||
setState({ percent: 0 })
|
||||
activePhase = undefined
|
||||
startSession(data.phase)
|
||||
sessionTransitions = transitions
|
||||
recalculate(toFiles(data.files), totals, {
|
||||
status: 'downloading',
|
||||
visible: dialogVisible(),
|
||||
reusedCount: data.reusedCount ?? 0,
|
||||
secondsLeft: undefined,
|
||||
dismissible: !transitions,
|
||||
dismissible: !sessionTransitions,
|
||||
})
|
||||
break
|
||||
case 'SW_PRECACHE_PROGRESS':
|
||||
// Durum isteği bir yarışa takılıp cevapsız kalırsa pencere burada açılır.
|
||||
if (!dialogMountRequested) void showUpdateDialog('downloading')
|
||||
// START/STATE'i kaçırmış olabiliriz; faz değiştiyse önceki oturum atılır.
|
||||
if (activePhase !== data.phase) {
|
||||
startSession(data.phase)
|
||||
sessionTransitions = transitions
|
||||
}
|
||||
recalculate(mergeFiles(toFiles(data.updates)), totals, {
|
||||
status: 'downloading',
|
||||
visible: dialogVisible(),
|
||||
dismissible: !transitions,
|
||||
dismissible: !sessionTransitions,
|
||||
})
|
||||
break
|
||||
case 'SW_PRECACHE_DONE':
|
||||
// Geçişsiz kurulum bittiğinde ısıtma yeniden söz sahibi olabilir.
|
||||
if (!sessionTransitions) {
|
||||
installSessionActive = false
|
||||
activePhase = undefined
|
||||
}
|
||||
recalculate(keepSeq(toFiles(data.files)), totals, {
|
||||
// Yeni sürüme geçilecekse 'ready'; ısıtmada iş bitti.
|
||||
status: transitions ? 'ready' : 'completed',
|
||||
visible: transitions || dialogVisible(),
|
||||
status: sessionTransitions ? 'ready' : 'completed',
|
||||
visible: sessionTransitions || dialogVisible(),
|
||||
percent: 100,
|
||||
})
|
||||
if (transitions) scheduleActivationHandoff()
|
||||
if (sessionTransitions) scheduleActivationHandoff()
|
||||
else hideDialogLater()
|
||||
break
|
||||
case 'SW_PRECACHE_ERROR':
|
||||
if (!transitions) {
|
||||
if (!sessionTransitions) {
|
||||
installSessionActive = false
|
||||
activePhase = undefined
|
||||
// Çalışan sürüm etkilenmedi; kullanıcıyı yeniden yükleme döngüsüne sokma.
|
||||
setState({
|
||||
status: 'error',
|
||||
|
|
@ -757,8 +810,9 @@ function requestPrecacheState(reg: ServiceWorkerRegistration) {
|
|||
* Worker eksik bir şey yoksa hiçbir mesaj yollamaz, pencere de açılmaz.
|
||||
*/
|
||||
function requestWarmup(reg: ServiceWorkerRegistration) {
|
||||
// Kurulum sürüyorsa zaten indiriliyor; ikinci bir oturum başlatma.
|
||||
if (reg.installing) return
|
||||
// Kurulum sürüyor ya da geçişi bekliyorsa ikinci bir oturum başlatma: iki
|
||||
// oturum hem bant genişliği için yarışır hem de pencerede iç içe geçer.
|
||||
if (reg.installing || reg.waiting || installSessionActive) return
|
||||
const worker = reg.active ?? navigator.serviceWorker.controller
|
||||
worker?.postMessage({ type: 'ENSURE_WARMUP' })
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue