@appaflytech/zeyrek-ai-mcp 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +46 -0
- package/README.md +260 -0
- package/dist/config.js +128 -0
- package/dist/index.js +88 -0
- package/dist/knowledge/componentCatalog.js +438 -0
- package/dist/knowledge/componentEvents.js +164 -0
- package/dist/knowledge/coverage.js +60 -0
- package/dist/knowledge/normalize.js +1309 -0
- package/dist/knowledge/outlines.js +167 -0
- package/dist/knowledge/policy.js +44 -0
- package/dist/knowledge/reservedTables.js +75 -0
- package/dist/knowledge/rules.js +1725 -0
- package/dist/observability/trace.js +337 -0
- package/dist/platform/errors.js +132 -0
- package/dist/platform/http.js +169 -0
- package/dist/platform/responses.js +94 -0
- package/dist/platform/token.js +79 -0
- package/dist/resources/examples/entity-pair-m2m.json +105 -0
- package/dist/resources/examples/flow-list-to-form.json +65 -0
- package/dist/resources/examples/flow-load-table.json +178 -0
- package/dist/resources/examples/microservice-table-list.json +49 -0
- package/dist/resources/examples/middleware-authenticate.json +194 -0
- package/dist/resources/examples/page-detail-profile.json +95 -0
- package/dist/resources/examples/page-dynamic-form.json +221 -0
- package/dist/resources/examples/page-form-upsert.json +279 -0
- package/dist/resources/examples/page-html-embed.json +106 -0
- package/dist/resources/examples/page-sign-in.json +194 -0
- package/dist/resources/examples/workflow-basic.json +94 -0
- package/dist/resources/guides/component.md +108 -0
- package/dist/resources/guides/entity.md +348 -0
- package/dist/resources/guides/expressions.md +139 -0
- package/dist/resources/guides/getting-started.md +109 -0
- package/dist/resources/guides/html.md +106 -0
- package/dist/resources/guides/microservice.md +313 -0
- package/dist/resources/guides/middleware.md +271 -0
- package/dist/resources/guides/modify-existing.md +35 -0
- package/dist/resources/guides/page.md +1574 -0
- package/dist/resources/guides/view.md +89 -0
- package/dist/resources/guides/workflow.md +359 -0
- package/dist/resources/knowledge.js +44 -0
- package/dist/resources/schemas/CreateDatasourceCommand.json +54 -0
- package/dist/resources/schemas/CreateDiscoveredEntitiesCommand.json +133 -0
- package/dist/resources/schemas/CreateEntityCommand.json +387 -0
- package/dist/resources/schemas/CreateMicroserviceCommand.json +528 -0
- package/dist/resources/schemas/CreateQueryCommand.json +43 -0
- package/dist/resources/schemas/CreateViewCommand.json +158 -0
- package/dist/resources/schemas/CreateWorkflowCommand.json +1719 -0
- package/dist/resources/schemas/Filter.json +124 -0
- package/dist/resources/schemas/GetRowsQuery.json +122 -0
- package/dist/resources/schemas/PreviewMicroserviceCommand.json +565 -0
- package/dist/resources/schemas/PreviewQueryCommand.json +74 -0
- package/dist/resources/schemas/PreviewWorkflowCommand.json +981 -0
- package/dist/resources/schemas/StepDto.json +900 -0
- package/dist/resources/schemas/UpdateDatasourceCommand.json +58 -0
- package/dist/resources/schemas/UpdateEntityCommand.json +679 -0
- package/dist/resources/schemas/UpdateMicroserviceCommand.json +512 -0
- package/dist/resources/schemas/UpdateQueryCommand.json +86 -0
- package/dist/resources/schemas/UpdateViewCommand.json +153 -0
- package/dist/resources/schemas/UpdateWorkflowCommand.json +1719 -0
- package/dist/resources/schemas.js +40 -0
- package/dist/server.js +63 -0
- package/dist/tools/access.js +27 -0
- package/dist/tools/applications.js +59 -0
- package/dist/tools/automations.js +103 -0
- package/dist/tools/common.js +18 -0
- package/dist/tools/components.js +280 -0
- package/dist/tools/datasources.js +82 -0
- package/dist/tools/dependencies.js +17 -0
- package/dist/tools/deployments.js +17 -0
- package/dist/tools/dtos.js +54 -0
- package/dist/tools/entities.js +166 -0
- package/dist/tools/files.js +138 -0
- package/dist/tools/flows.js +226 -0
- package/dist/tools/folders.js +51 -0
- package/dist/tools/layouts.js +105 -0
- package/dist/tools/microservices.js +198 -0
- package/dist/tools/organizations.js +65 -0
- package/dist/tools/pages.js +597 -0
- package/dist/tools/platformInfo.js +44 -0
- package/dist/tools/projects.js +152 -0
- package/dist/tools/queries.js +48 -0
- package/dist/tools/rows.js +115 -0
- package/dist/tools/screens.js +97 -0
- package/dist/tools/templates.js +65 -0
- package/dist/tools/variables.js +94 -0
- package/dist/tools/views.js +58 -0
- package/dist/tools/workflows.js +196 -0
- package/package.json +49 -0
|
@@ -0,0 +1,1574 @@
|
|
|
1
|
+
# Sayfa Tasarımı Rehberi (page, layout, component, flow)
|
|
2
|
+
|
|
3
|
+
Studio'nun tuval tarafı. Buradaki kurallar controller + komut kodundan doğrulandı
|
|
4
|
+
(`zeyrek-mcp/docs/13-faz2-envanter.md` §4); veri/mantık tarafı için
|
|
5
|
+
`platform_guide('entity')`, `('microservice')`, `('workflow')`.
|
|
6
|
+
|
|
7
|
+
## Kuruluş sırası
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
1. layout_list / layout_create (layout isteğe bağlı; sayfalar layoutId ile bağlanır)
|
|
11
|
+
2. role_list (roleIds'i buradan oku — rol id'si TAHMİN EDİLMEZ)
|
|
12
|
+
3. component_list / component_get (konulacak tipin prop sözlüğü + boyut sınırları)
|
|
13
|
+
4. page_create (şemasız aç; şema micro-op hattından kurulur)
|
|
14
|
+
5. page_state (değişkenler, komponentler, layout slotları, ms imzaları)
|
|
15
|
+
6. page_schema_patch (bütün add_component'ler — tek çağrıda)
|
|
16
|
+
7. page_schema_patch (tema + stil: page_container.style'da --ng-* ve komponent
|
|
17
|
+
style'ları; ifadeye bağlı prop'lar için set_binding)
|
|
18
|
+
8. flow_create × N (BÜTÜN akışlar — bağlı akış varken yenisi eklenemez, Kural 3)
|
|
19
|
+
9. page_schema_patch (bütün set_event'ler tek çağrıda)
|
|
20
|
+
10. page_get + page_runtime_get (doğrulama: bağlar doğru mu, son hâl nasıl çiziliyor)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
**Ekranın tipi hangi kuralı okuyacağını belirler:** alanları tasarım anında bilinen klasik form
|
|
24
|
+
→ Kural 7a; **alan sayısı/tipi veriden geliyorsa** (anket, dinamik başvuru) → **Kural 1g**
|
|
25
|
+
(`repeater`, `html`'e gerek yok); görsel kalite ve marka teması → **Kural 1h** (hazır stil
|
|
26
|
+
blokları 1h-3'te).
|
|
27
|
+
|
|
28
|
+
## Kural 1 — Komponent gövdesinin şekli (`DesignComponentManagementSchema`)
|
|
29
|
+
|
|
30
|
+
Sayfa şeması **micro-op hattından** kurulur; `page_create`'e satır içi `schema` vermek de
|
|
31
|
+
mümkündür ama doğrulama duvarı ve canlı canvas yalnız `page_schema_patch`'te çalışır.
|
|
32
|
+
|
|
33
|
+
```jsonc
|
|
34
|
+
{ "elementId": "page_header_1", "kind": "component", "type": "<component_list slug'ı>",
|
|
35
|
+
"name": "Page header",
|
|
36
|
+
"props": { "pageTitle": { "type": "static", "data": "Branşlar" } },
|
|
37
|
+
"states": { "isSticky": false, "isDroppable": false, "isScrollable": false, "isWrapper": false },
|
|
38
|
+
"blocks": [{ "breakpoint": 1024, "size": { "h": 4, "w": 12 }, "position": { "x": 0, "y": 0 } }],
|
|
39
|
+
"children": [], "events": [], "validations": [], "stateBindings": [] }
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
- **`props` düz sözlük DEĞİL:** her değer bir `Prop` nesnesidir ve alanları `type` / `ref` /
|
|
43
|
+
`data`'dır — **`value` diye bir alan yoktur.** Static prop için `{ "type": "static", "data": … }`
|
|
44
|
+
yaz; `{ "value": … }` yazarsan platform sessizce boş prop kaydeder
|
|
45
|
+
(`DesignComponentProperty.SetRef` static'te `value.Data` okur).
|
|
46
|
+
- **Prop'un içi de Prop'tur (özyinelemeli).** Bir Prop'un `data`'sı nesne ya da dizi ise
|
|
47
|
+
platform içini **her seviyede** `Dictionary<string, Prop>` olarak çözer
|
|
48
|
+
(`CreateProp` object/array dalları) — düz bırakılan iç değer `JsonException … could not be
|
|
49
|
+
converted to Prop` verir. Skaler dizi (`[1,2,3]`) hiçbir biçimde desteklenmez. Doğru şekil
|
|
50
|
+
(header `navigation` örneği — iç `icon` nesnesinin İÇİ de sarılı):
|
|
51
|
+
|
|
52
|
+
```jsonc
|
|
53
|
+
"navigation": { "type": "static", "data": [
|
|
54
|
+
{ "title": { "type": "static", "data": "Kitaplar" },
|
|
55
|
+
"href": { "type": "static", "data": "/kitaplar" },
|
|
56
|
+
"icon": { "type": "static", "data": {
|
|
57
|
+
"name": { "type": "static", "data": "book" },
|
|
58
|
+
"color": { "type": "static", "data": "#0052CC" } } } }
|
|
59
|
+
] }
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**Gateway eksik sarımı kendisi tamamlıyor** (`gatewayNormalized`: "iç değerler Prop'a
|
|
63
|
+
sarıldı") — düz `{name, color}` yazsan da düzeltilir; ama ham HTTP kullanıyorsan bu kural sana ait.
|
|
64
|
+
Sarım `add_component` / `replace_prop` op'larında, `page_create`/`page_update` ve `layout_*`
|
|
65
|
+
gövdelerinde ve artık `page_schema_validate` KURU KOŞUSUNDA da uygulanır (crm koşumu B-11'de
|
|
66
|
+
kuru koşu ile yazma farklı davranıyordu; ikisi eşitlendi). Platformun kendisi sarmıyor:
|
|
67
|
+
`columns` · `colorMap` · `actions` · `options` gibi **dizi prop'larının her yaprağı**
|
|
68
|
+
`{ type:'static', data: … }` olmak zorunda, aksi hâlde `to-domain: The JSON value could not be
|
|
69
|
+
converted to … Prop. Path: $.name` alırsın (hata komponenti/prop'u söylemez).
|
|
70
|
+
- **Enum'lar küçük harf** (`StringEnumConverter` + `[Description]`): `kind` → `component` |
|
|
71
|
+
`widget`, prop `type` → `static` | `file` | `flow` | `widget` | `element` | `page` | `code`.
|
|
72
|
+
- **Sayfaya bağlanan prop'larda (link/yönlendirme) URL yazma, `type: 'page'` + `ref: <pageId>`
|
|
73
|
+
kullan.** Platform slug'ı kendisi çözer (`Prop(PropValueType.Page)` → `Ref = RedirectPageId`,
|
|
74
|
+
`Data = RedirectPage.Slug`). Sabit `'/hastalar'` yazarsan sayfa slug'ı değişince bağlantı
|
|
75
|
+
sessizce kırılır; `pageId` referansı değişiklikten etkilenmez. Aynısı `flow` prop'u için
|
|
76
|
+
`type: 'flow'` + `ref: <flowId>`, dosya için `type: 'file'` + `ref: <fileId>`.
|
|
77
|
+
- **`blocks[].size`, komponentin `component_get`'teki `maxSize`'ını aşamaz.** Dikkat: katalog
|
|
78
|
+
varsayılanı aşabiliyor (`page-header` default `h:8`, `maxSize` `h:4`) — varsayılanı körü körüne
|
|
79
|
+
kopyalama, doğrulayıcı `"Height of 'page-header' must be between 1 and 4."` ile reddeder.
|
|
80
|
+
- **Aynı ızgara hücrelerini iki komponente veremezsin.** Studio'da komponent tuvale bırakıldığında
|
|
81
|
+
çakışan kardeşleri aşağı itme işini studio-api'nin sayfa kurma servisi yapar
|
|
82
|
+
(`SchemaHelper.reorderComponents` → `draggableElement.moveItem`); komple şema yazan `page_create`
|
|
83
|
+
/ `page_update` o servisten geçmez, yani verdiğin koordinatlar olduğu gibi kayda girer ve tuvalde
|
|
84
|
+
komponentler üst üste biner. **Gateway aynı algoritmayı şemanın tamamına uyguluyor** (kök dizi +
|
|
85
|
+
her `children`, yalnız desktop `blocks[]` girdisi; `x`/`w`/`h` ve dizi sırası korunur, çakışanın
|
|
86
|
+
`position.y`'si aşağı kayar) ve yanıtta `gatewayNormalized: "çakışan komponentler ızgarada aşağı
|
|
87
|
+
kaydırıldı (…)"` ile bildiriyor. Kökteki `modal`/`drawer` katmanları sayfa akışında durmadığı için
|
|
88
|
+
hariç. Aynı hücreyi paylaşan komponentlerden hangisinin üstte kalacağı garanti değil — konumu sen
|
|
89
|
+
doğru ver, düzeltme yalnız emniyet kemeri.
|
|
90
|
+
- **Zorunlu alanlar:** `states` her yolda, `events` / `validations` / `stateBindings` ise
|
|
91
|
+
**micro-op yolunda** (`page_schema_patch`). `states` için sebep açık:
|
|
92
|
+
`DesignComponentManagementSchema.ToDomain:47` guard'sız okur, `Appearance`'ın varsayılanı var
|
|
93
|
+
`States`'in yok. Diğer üçünü `page_create` (POST /pages) tolere ediyor ama `PATCH …/schema`
|
|
94
|
+
etmiyor — canlı deneyde üçü **birlikte** verilince geçti, her biri **tek başına** verilince
|
|
95
|
+
`NullReferenceException` aldı. **Gateway hepsini senin yerine dolduruyor**
|
|
96
|
+
(`page_create`/`page_update`/`page_schema_patch`, `layout_create/update`,
|
|
97
|
+
`component_create/update`) ve yanıtta `gatewayNormalized` ile bildiriyor; yani elle yazman
|
|
98
|
+
gerekmiyor, ama ham HTTP kullanıyorsan bu farkı bilmen gerekir.
|
|
99
|
+
|
|
100
|
+
> **`page_schema_validate`'e tek başına güvenme.** `ValidatePageSchemaQuery` yalnız micro-op'ları
|
|
101
|
+
> uygulayıp şema kurallarını denetler, `ToDomain`'e hiç girmez — eksik `states`'li bir gövdeye
|
|
102
|
+
> `{"valid":true}` der, yazma yolu ise patlar. Gövde tamamlandığında ise doğrulayıcı işini
|
|
103
|
+
> düzgün yapar (boyut/hedef hatalarını açık mesajla verir).
|
|
104
|
+
|
|
105
|
+
## Kural 1b — `page_build` YOK
|
|
106
|
+
|
|
107
|
+
Entity'den otomatik sayfa üreten `page_build` tool'u **kaldırıldı**. Platformun
|
|
108
|
+
`POST /pages/{app}/build/{page}` ucu üretimi `PageBuilderApiOptions:Url` adresindeki **ayrı bir
|
|
109
|
+
servise** delege ediyor (`PageBuilderApiClient`); dev'de `localhost:4000`, prod'da `studio-api…`.
|
|
110
|
+
O servis bu ürünün parçası değil ve ayakta olmayabilir. Sayfa/komponent şemasının tam karşılığı
|
|
111
|
+
platformda zaten var → doğru yol yukarıdaki micro-op hattıdır.
|
|
112
|
+
|
|
113
|
+
## Kural 1c — Komponente VERİ bağlama: `mount` olayı + flow (setter adımı)
|
|
114
|
+
|
|
115
|
+
Tablonun/listenin satırları şemaya "dataSource" yazarak değil, **flow ile** doldurulur. Desen:
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
1. component_get → komponentin EVENT listesini oku (event'ler komponente özeldir, tahmin edilmez)
|
|
119
|
+
2. flow_create → start → microservice → setter → end
|
|
120
|
+
3. page_schema_patch set_event → akışı ilgili olaya bağla
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**Event'ler komponent tipine göre değişir; `component_get`'in `events` alanından okunur.**
|
|
124
|
+
Sayfa açılışında veri çekmek için `mount` kullanılır ama tek seçenek o değil — örneğin `table`
|
|
125
|
+
8 olay yayar:
|
|
126
|
+
|
|
127
|
+
`mount · pagination · filter · sort · select · selectionChange · doubleClick · rowChange`
|
|
128
|
+
|
|
129
|
+
Yani "satırı seçince detay getir" (`select`), "kolondan süzünce sunucudan yeniden çek"
|
|
130
|
+
(`filter`), "sayfa değişince yeni sayfayı çek" (`pagination`), "satıra çift tıklayınca formu aç"
|
|
131
|
+
(`doubleClick`) akışları da aynı `set_event` mekanizmasıyla kurulur. Buna karşılık `page-header`
|
|
132
|
+
ve `datatable` yalnız `mount` yayar (`datatable`'ınki bilinçli — Kural 1e).
|
|
133
|
+
|
|
134
|
+
**Olay adları `on` ÖNEKİ ALMAZ** (`button` → `click`, `form` → `submit`), tek istisna katalogda
|
|
135
|
+
`texteditor`'dur (`onMount`/`onChange`). **Komponentin desteklemediği bir olay adı verirsen bağ
|
|
136
|
+
sessizce ölü kalır** — önce `component_get`. Gateway `set_event`'i katalogla karşılaştırır ve
|
|
137
|
+
uymayan adı **PAGE-008** ile istek gitmeden reddeder.
|
|
138
|
+
|
|
139
|
+
**Olay KALDIRMAK için `remove_event` kullan.** Platformun `set_event` micro-op'u olay silemez:
|
|
140
|
+
`eventValue: null` verildiğinde kayıt `{ name: 'click' }` olarak (değersiz) kalır, diziden
|
|
141
|
+
çıkmaz — şemada ölü bağ artığı bırakır. `{ "op": "remove_event", "target": "<elementId>",
|
|
142
|
+
"event": "click" }` gateway emülasyonudur: şemayı okur, olayı diziden çıkarır ve sayfayı tam
|
|
143
|
+
gövdeyle geri yazar (yanıtta `removedEvents`). Sayfa düzeyi olaylar için `target: "page"`.
|
|
144
|
+
|
|
145
|
+
**Olay YÜKÜ komponente özeldir ve `component_get` onu da veriyor** (`events[].payload`, gerekirse
|
|
146
|
+
`events[].reads`) — **ama yük akışa `event.data` ile GELMEZ.** Motor emit'in `data`'sını önce
|
|
147
|
+
komponentin KENDİ prop'una yazar (`FlowMachine.onEmit` → `setNode`), akışı ise emit'in
|
|
148
|
+
`params`'ıyla başlatır; ifade kapsamındaki `event.data` `params.data`'dır ve komponent
|
|
149
|
+
olaylarında **boştur** (ölçüm 2026-08-25, kaynak okuması: `flow-machine.ts:299-318` +
|
|
150
|
+
`flow-execution.functions.ts:236`; `Table.svelte:292` kendi yorumunda aynı şeyi söylüyor).
|
|
151
|
+
|
|
152
|
+
Pratik kural: **`payload` anahtarları aynı zamanda prop adlarıdır; akışta `self.<anahtar>` ile
|
|
153
|
+
oku** (ya da `value { type:'property', ref:{ source:'component', element, property } }` ile).
|
|
154
|
+
`event.data` yalnız `params` ile gelen olaylarda dolar — pratikte `html` komponentinin
|
|
155
|
+
`fireEvent` aksiyonları (`platform_guide('html')`). Sessiz hata örneği: sayfalama akışını
|
|
156
|
+
`event.data.limit`'e bağlarsan sorgu `LIMIT null OFFSET null` ile koşar ve **tüm satırları**
|
|
157
|
+
döndürür; hata yok, sayfalama yok.
|
|
158
|
+
|
|
159
|
+
| Komponent · olay | Yük (aynı zamanda prop adları) | Akışta nereden okunur |
|
|
160
|
+
|---|---|---|
|
|
161
|
+
| `table.sort` | `{ sortField, sortType, offset }` | `self.sortField` · `self.sortType` · `self.offset` |
|
|
162
|
+
| `table.filter` | `{ filters, offset }` | `self.filters` · `self.offset` |
|
|
163
|
+
| `table.pagination` | `{ offset, limit }` | `self.offset` · `self.limit` |
|
|
164
|
+
| `table.select` · `doubleClick` | — | `self.selectedRow` |
|
|
165
|
+
| `table.selectionChange` | — | `self.selectedRows` |
|
|
166
|
+
| `datatable.mount` | — | `self.filters` · `self.sortField` · `self.sortType` · `self.offset` |
|
|
167
|
+
| `pagination.change` | `{ offset, limit }` | `self.offset` · `self.limit` |
|
|
168
|
+
| `textinput/textarea/password.change` | `{ value }` | `self.value` |
|
|
169
|
+
| `dropdown.change` · `multi-select.change` | `{ value }` | `self.value` |
|
|
170
|
+
| `dropdown.search` · `multi-select.search` | `{ searchText }` | `self.searchText` |
|
|
171
|
+
| `tabs.tab` | `{ selected }` | `self.selected` |
|
|
172
|
+
| `chart.dataPointClick` | `{ name, seriesName, value, dataIndex, row }` | `self.row` (kaynak satırın tamamı) |
|
|
173
|
+
| `repeater.change` | `{ selectedIndex }` | `item` / `Row` satır bağlamı |
|
|
174
|
+
| `html` aksiyonu (`fireEvent`) | ikinci argüman | **`event.data`** (tek istisna) |
|
|
175
|
+
|
|
176
|
+
Adım gövdesi `StepManagementSchema`: `{ stepId, name, type, kind, args, branches }`.
|
|
177
|
+
**`branches[].nextStepId` bir GUID değil, hedef adımın `stepId` STRING'idir**
|
|
178
|
+
(`allSteps.TryGetValue(b.NextStepId)`).
|
|
179
|
+
|
|
180
|
+
Bu adımda platformun sessizce yuttuğu beş tuzak var — hepsi isteği 200 döndürür, akış yine de
|
|
181
|
+
ölü ya da yarım çalışır. Gateway hepsini yakalar (FLOW-004…012), ama şekli baştan doğru yazın:
|
|
182
|
+
|
|
183
|
+
**1. Dal adı motorun anahtarıdır, etiket değil.** Runtime dal kimliğini `branch.name`'den üretir
|
|
184
|
+
(`Runtime.svelte:329`) ve her adım `engine.next('always')` çağırır. Ad `always` değilse motor
|
|
185
|
+
dalı bulamaz ve akışı **sessizce durdurur** (`flow-engine.ts:79`); Studio da zinciri
|
|
186
|
+
`sourceBranch === 'always'` ile çizdiği için panelde **Start kartından sonrası boş görünür**.
|
|
187
|
+
İstisnalar: `condition` → dal adı rule id'sidir, `confirm` → `yes|no|always`, `microservice` →
|
|
188
|
+
`always` (eski akışlarda `success` de çalışır).
|
|
189
|
+
|
|
190
|
+
**2. `iteratees[].type` yalnız `change` | `fill`.** `setter.ts` başka değerde hiçbir dala
|
|
191
|
+
girmez — adım çalışır, hiçbir şey yapmaz. `change` tek prop'a değer yazar, `fill` diziyi
|
|
192
|
+
`mapping` ile eşleyerek yazar.
|
|
193
|
+
|
|
194
|
+
**3. Storage anahtarı adımın ADIdır, `stepId` değil.** Sonuç `setStorage(task.name, …)` ile
|
|
195
|
+
yazılır; `isRaw: false` iken sarmal `{ data, headers, error }` olduğu için ref
|
|
196
|
+
`"<adım adı>.data"` olur. Adım adında **nokta kullanmayın** (okuma dot-prop ile yapılır).
|
|
197
|
+
Kod ifadesinden okuma biçimi `Steps["<adım adı>"].data`'dır; `value.type: "code"` + ref olarak
|
|
198
|
+
düz `stepId` yazmak tanımsız değişken hatası verir. **Anahtarı iki yerden verebilirsin ve
|
|
199
|
+
öncelik `mapping.source`'tadır:** doluysa `value.ref`'e hiç bakılmaz (`setter.ts:161`) — satır
|
|
200
|
+
eşlemeli tablo yazımında Studio'nun ürettiği şekil de budur (`mapping.source: "<ad>.data"`,
|
|
201
|
+
`value.ref: ""`). İkisini birden doldurma.
|
|
202
|
+
|
|
203
|
+
**4. Hata yolunda storage sarmalında `headers` YOKTUR.** Başarıda `{ data, headers, error }`,
|
|
204
|
+
hatada `{ error, data }` yazılır (`microservice.ts:132`). Bu yüzden toplam kayıt sayısını
|
|
205
|
+
guard'sız okumak (`Number(Steps.get_service.headers["total-count"])`) istek patladığı anda
|
|
206
|
+
TypeError verir: prop'a `NaN` yazılır, ifade hatası toast'ı düşer ve akış `onError`'a düşebilir.
|
|
207
|
+
Doğrusu `Number(Steps.get_service?.headers?.["total-count"] ?? 0)`. Header adı backend'de
|
|
208
|
+
`Total-Count`'tur, axios anahtarları küçük harfe indirir → `"total-count"`.
|
|
209
|
+
|
|
210
|
+
**5. `mapping.mappings` anahtarı satıra DÜZ yazılır.** `object[key] = …` (`setter.ts:83-104`) —
|
|
211
|
+
`"parent.name"` anahtarı `row["parent.name"]` üretir, `row.parent.name` üretmez. Kolonun kendi
|
|
212
|
+
key'ini düz anahtar ver, iç içe yolu `ref`'e yaz: `"parent_name": { "type": "item", "ref":
|
|
213
|
+
"parent_title.name" }`. Anahtar kümesini tablonun gerçek kolonlarıyla sınırlı tut; her fazladan
|
|
214
|
+
anahtar her satırda taşınır. `type` sözlüğü: `item` (kaynak alanı), `code` (satır başına JS,
|
|
215
|
+
satır `Row` değişkenindedir), diğerleri sabit.
|
|
216
|
+
|
|
217
|
+
```jsonc
|
|
218
|
+
// akışı başlatan adım — dal adı 'always' olmak zorunda
|
|
219
|
+
{ "stepId": "start", "name": "Start", "type": "start", "kind": "normal", "args": {},
|
|
220
|
+
"branches": [{ "id": "<GUID>", "name": "always", "nextStepId": "load_specialties" }] }
|
|
221
|
+
|
|
222
|
+
// veriyi çeken adım
|
|
223
|
+
{ "stepId": "load_specialties", "name": "Branşları getir", "type": "microservice", "kind": "normal",
|
|
224
|
+
"args": { "microserviceId": "<ms id>", "isRaw": false, "parameters": [] },
|
|
225
|
+
"branches": [{ "id": "<GUID>", "name": "always", "nextStepId": "set_rows" }] }
|
|
226
|
+
|
|
227
|
+
// prop'a yazan adım
|
|
228
|
+
{ "stepId": "set_rows", "name": "Tabloya yaz", "type": "setter", "kind": "normal",
|
|
229
|
+
"args": { "iteratees": [{
|
|
230
|
+
"id": "<GUID>",
|
|
231
|
+
"type": "change", // yalnız change | fill
|
|
232
|
+
"element": "specialties_table", // hedef komponentin elementId'si
|
|
233
|
+
"property": "rows", // component_get'teki prop adı
|
|
234
|
+
"source": "component",
|
|
235
|
+
"mapping": { "source": "", "mappings": {} }, // storage okurken zorunlu, boş da olsa
|
|
236
|
+
"value": { "type": "storage", "ref": "Branşları getir.data" } }] }, // kaynak adımın ADI
|
|
237
|
+
"branches": [{ "id": "<GUID>", "name": "always", "nextStepId": null }] } // son adım: null
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
> **`element` ve `property` İKİSİ DE zorunludur — biri eksikse adım sessizce ölür.** `setter.ts`
|
|
241
|
+
> `change()` ve `fill()` her dalda `machine.setNode(element, { [property]: value })` çağırır;
|
|
242
|
+
> `element` boşken çağrı hiçbir düğüme denk gelmez: **adım çalışır, hata vermez, hiçbir şey
|
|
243
|
+
> yazmaz.** Platform gövdeyi 200 ile kabul eder (alan serbest string) ve Studio adımı çizer —
|
|
244
|
+
> ekranda "değer girilmiş ama hedef seçilmemiş" bir adım görürsün. Ölçüm (anket-spike,
|
|
245
|
+
> 2026-08-19): `html` komponentine `context` yazan iki akış da yalnız bu yüzden hiç çalışmıyordu.
|
|
246
|
+
> Platform bu alanları doğrulamaz; tek savunma gateway'in **FLOW-017** kuralıdır — gövdeyi istek
|
|
247
|
+
> göndermeden reddeder. `element` değerini `page_schema_get` çıktısındaki `elementId` ile doğrula,
|
|
248
|
+
> uydurma.
|
|
249
|
+
|
|
250
|
+
Satır eşlemeli tablo yüklemesinin tam şekli (yükleniyor bayrağı → uç → satırlar + toplam kayıt)
|
|
251
|
+
`ngapps://examples/flow-load-table`'dadır; setter'ın kritik iki iteratee'si şöyle görünür:
|
|
252
|
+
|
|
253
|
+
```jsonc
|
|
254
|
+
// toplam kayıt — guard'sız yazarsan hata yolunda patlar
|
|
255
|
+
{ "type": "change", "element": "titles_table", "property": "totalRows", "source": "component",
|
|
256
|
+
"mapping": { "source": "", "mappings": {} },
|
|
257
|
+
"value": { "type": "code", "ref": "Number(Steps.load_titles?.headers?.[\"total-count\"] ?? 0)" } }
|
|
258
|
+
|
|
259
|
+
// satırlar — anahtar mapping.source'ta, mapping anahtarları DÜZ ve kolon key'leriyle birebir
|
|
260
|
+
{ "type": "change", "element": "titles_table", "property": "rows", "source": "component",
|
|
261
|
+
"mapping": { "source": "load_titles.data", "mappings": {
|
|
262
|
+
"name": { "type": "item", "ref": "name", "propertyType": "text" },
|
|
263
|
+
"parent_name": { "type": "item", "ref": "parent_title.name", "propertyType": "text" },
|
|
264
|
+
"status": { "type": "code", "ref": "Row.status === 'Active' ? 'Aktif' : 'Pasif'", "propertyType": "text" } } },
|
|
265
|
+
"value": { "type": "storage", "ref": "" } }
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
`errorSteps` verirsen (start dışında en az bir adım) hata anında `engine.onError` true döner ve
|
|
269
|
+
**ana akışın kalan adımları hiç çalışmaz** — yükleniyor bayrağını kapatmak, hata mesajını
|
|
270
|
+
göstermek error flow'un işidir (`alert` adımı: `args: { message, alertType: 'error' }`).
|
|
271
|
+
|
|
272
|
+
Zorunlu filtresi olan uçlarda (`patient-*`, `doctor-appointments` gibi `isRequired: true`
|
|
273
|
+
filtreli microservice'ler) parametreyi adımda ver, yoksa uç `MissingParameterException` atar:
|
|
274
|
+
|
|
275
|
+
```jsonc
|
|
276
|
+
"parameters": [{ "name": "patient_id", "type": "guid", "source": "query-string",
|
|
277
|
+
"value": { "ref": "<hasta id>", "type": "static" } }]
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
Gerçek uygulamada `static` yerine sayfa değişkeni / route parametresi kullanılır
|
|
281
|
+
(`variable_create` + `value.type: 'variable'`). **Kullanıcıya özel veri çeken uçlarda
|
|
282
|
+
(hasta/doktor/oturum sahibi) kalıcı çözüm parametre DEĞİL middleware'dir:** uca auth
|
|
283
|
+
middleware'i bağlanır, filtre `source:'expression'` + `value:'user.id'` olur — kimlik
|
|
284
|
+
istemciden gelmediği için sahtelenemez ve sayfada sabit id tutmak gerekmez. Sayfanın kendisi de
|
|
285
|
+
`page_create/update` `middlewareIds` ile korunur (girişsiz kullanıcıyı login'e yönlendirme).
|
|
286
|
+
Tarif: `platform_guide('middleware')`.
|
|
287
|
+
|
|
288
|
+
**`flow-trigger` adımının İKİ geçerli şekli var** (`flow-trigger.ts`; başka şekil sessizce hiçbir
|
|
289
|
+
şey yapmaz, ikisi de yoksa kayıt 404 döner):
|
|
290
|
+
|
|
291
|
+
```jsonc
|
|
292
|
+
// (1) başka bir akışı çalıştır — "kaydettim, listeyi yenile"
|
|
293
|
+
{ "stepId": "reload_list", "name": "reload_list", "type": "flow-trigger", "kind": "normal",
|
|
294
|
+
"args": { "flowId": "<hedef flow id>" },
|
|
295
|
+
"branches": [{ "id": "<GUID>", "name": "always", "nextStepId": null }] }
|
|
296
|
+
|
|
297
|
+
// (2) bir komponentin OLAY akışını çalıştır — Studio'nun ürettiği şekil budur
|
|
298
|
+
{ "stepId": "refresh_table", "name": "refresh_table", "type": "flow-trigger", "kind": "normal",
|
|
299
|
+
"args": { "triggerElementId": "tickets_table", "triggerElementEvent": "mount" },
|
|
300
|
+
"branches": [{ "id": "<GUID>", "name": "always", "nextStepId": null }] }
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
- İkisi birden verilirse runtime **yalnız `flowId`'ye** bakar, element dalına hiç girmez.
|
|
304
|
+
- (2)'de `triggerElementId` sayfadaki bir **elementId**'dir ve `triggerElementEvent` o komponentin
|
|
305
|
+
**gerçekten yaydığı** bir olay olmalı; ayrıca o olayın bir akışa bağlı olması gerekir
|
|
306
|
+
(`events[].value` dolu), yoksa çalıştıracak bir şey bulunamaz.
|
|
307
|
+
- Adım hedef akışı **bekler** (`await engine.run`), sonra `always` dalından devam eder.
|
|
308
|
+
- Gateway boş/yarım `args`'ı **FLOW-014** ile yakalar.
|
|
309
|
+
|
|
310
|
+
**Sözlükler** (`StepType.cs`, `StepParameterValueType.cs` — hepsi küçük harf):
|
|
311
|
+
`start · microservice · setter · setcookie · validation · condition · redirect · alert ·
|
|
312
|
+
confirm · flow-trigger · storage · navigate · code · download · end`
|
|
313
|
+
değer tipleri: `static · page · property · storage · variable · code · item · state`
|
|
314
|
+
|
|
315
|
+
> `code` adımının içinde ve `value.type:'code'` ifadelerinde kullanılabilen bağlam
|
|
316
|
+
> (`Page.*`, komponentlere adıyla erişim, `state.page/app`, `Steps`, `self`/`item`/`event`,
|
|
317
|
+
> `cookie.get/set`, `await`) ve `value.type` çözümleme tablosu için
|
|
318
|
+
> **`platform_guide('expressions')`** — uydurma erişim adı sessizce `undefined` üretir.
|
|
319
|
+
|
|
320
|
+
> Bir microservice adımı kaydedildiğinde platform `args.microserviceSchema`'yı
|
|
321
|
+
> (parameterScheme + returnScheme + `isReturnArray`) kendisi doldurur — `flow_get` ile geri
|
|
322
|
+
> okuyunca hangi alanların döndüğünü oradan görürsün.
|
|
323
|
+
|
|
324
|
+
**Sayfalama üç parçadır; biri eksikse tablo sessizce tek sayfa kalır.** Uç `limit`/`offset`'i
|
|
325
|
+
**query-string'den** okur (`sourceParameters.limit: { name, offset, source }`) ve
|
|
326
|
+
`TableMicroserviceExecutor.cs:178` sayfalamayı **yalnız limit doluysa** uygular — parametre
|
|
327
|
+
geçmezsen sunucu her çağrıda tüm kayıtları döndürür, `pagination` olayı yeni sayfa çekemez.
|
|
328
|
+
Gereken üçlü:
|
|
329
|
+
|
|
330
|
+
```jsonc
|
|
331
|
+
// 1) akışta: uca tablonun kendi prop'larını geçir
|
|
332
|
+
"parameters": [
|
|
333
|
+
{ "name": "limit", "type": "text", "source": "query-string",
|
|
334
|
+
"value": { "type": "property", "ref": { "source": "component", "element": "blog_table", "property": "limit" } } },
|
|
335
|
+
{ "name": "offset", "type": "text", "source": "query-string",
|
|
336
|
+
"value": { "type": "property", "ref": { "source": "component", "element": "blog_table", "property": "offset" } } }
|
|
337
|
+
]
|
|
338
|
+
// 2) şemada: { "op": "replace_prop", "target": "blog_table", "prop": "isPaginated", "value": true }
|
|
339
|
+
// 3) şemada: { "op": "set_event", "target": "blog_table", "event": "pagination", "eventValue": "<load flow id>" }
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
Toplam kayıt sayısı `Total-Count` header'ından gelir (`TableMicroserviceExecutor.cs:201`, uç
|
|
343
|
+
`isSingle: false` olduğu sürece) — `totalRows`'u yukarıdaki guard'lı ifadeyle yaz. Gateway
|
|
344
|
+
parametreleri geçmeyi unutursan `FLOW-013` ile uyarır.
|
|
345
|
+
|
|
346
|
+
> **`replace_prop` Prop sarmalı DEĞİL ham değer alır** — `PageSchemaMicroOpApplier.cs:77`
|
|
347
|
+
> `new Prop(PropValueType.Static) { Data = op.Value }` kurar. `{ "type": "static", "data": true }`
|
|
348
|
+
> gönderirsen sarmalın kendisi `Data`'ya yazılır ve platform onu `Dictionary<string, Prop>` diye
|
|
349
|
+
> çözmeye çalışıp **500 JsonException** verir. Doğrusu `"value": true`; kod prop'u için `value`
|
|
350
|
+
> değil `expression` alanı kullanılır. (Gateway her ikisini de düzeltip `gatewayNormalized` ile
|
|
351
|
+
> bildirir, ama ham HTTP'de bu kural sana ait.) `add_component`'te ise sarmal ZORUNLUdur — iki
|
|
352
|
+
> op'un şekli birbirinin tersidir, karıştırma.
|
|
353
|
+
|
|
354
|
+
## Kural 1c-2 — Komponentin `name`'i JS TANIMLAYICISI olmalı (yoksa sayfadaki tüm ifadeler ölür)
|
|
355
|
+
|
|
356
|
+
Runtime, ifade çalıştırmadan önce sayfadaki **her** komponent için bir değişken üretir:
|
|
357
|
+
|
|
358
|
+
```js
|
|
359
|
+
const <komponentin name'i> = Components["<komponentin name'i>"];
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
`name` geçerli bir JS tanımlayıcısı değilse (boşluk, nokta, birden çok tire…) üretilen satır
|
|
363
|
+
sözdizimi hatası olur ve **o sayfadaki BÜTÜN ifadeler** — condition kuralları, `value.type:'code'`
|
|
364
|
+
ref'leri, `code` adımları — tek seferde ölür:
|
|
365
|
+
|
|
366
|
+
```
|
|
367
|
+
Expression error: Missing initializer in const declaration in <senin ifaden>
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
Belirti kafa karıştırıcıdır: hata **ifadeyi** gösterir, oysa suçlu bambaşka bir komponentin adıdır.
|
|
371
|
+
Ölçüm (2026-08-18, `ik`): `name: "Giriş Kutusu"` yüzünden giriş sayfası hiç çalışmadı — servis 200
|
|
372
|
+
dönüyor, condition patlıyor, kullanıcı yönlenmiyordu.
|
|
373
|
+
|
|
374
|
+
**Kural:** `name` **her zaman `elementId` ile aynı** olsun (`btn_login`, `tbl_mng`). Okunabilir
|
|
375
|
+
Türkçe başlık `props.title` / `props.text` / `props.label` / `props.pageTitle` içine yazılır —
|
|
376
|
+
`name` bir etiket değil, **değişken adıdır**.
|
|
377
|
+
|
|
378
|
+
> **Layout adları her sayfaya sızar.** Layout'un komponentleri de aynı `const` listesine girer;
|
|
379
|
+
> layout'ta tek bir bozuk ad, o layout'a bağlı **her** sayfayı öldürür. `layout_update` yaparken de
|
|
380
|
+
> `name = elementId` kuralı geçerlidir.
|
|
381
|
+
>
|
|
382
|
+
> Aynı sebeple akış ifadelerinde komponente **`name` ile** erişilir (`tbl_mng.selectedRow`);
|
|
383
|
+
> `elementId` ile eşit tutmazsan `element:` alanı çalışır ama ifadedeki değişken `undefined` kalır.
|
|
384
|
+
>
|
|
385
|
+
> Gateway bunu **PAGE-009** ile `add_component`, `page_update(schema)` ve `layout_update(views)`
|
|
386
|
+
> yollarında istek gitmeden reddeder.
|
|
387
|
+
|
|
388
|
+
## Kural 1d — Sayfa iskeleti: `page_container` ve boyutlandırma
|
|
389
|
+
|
|
390
|
+
**Her sayfada `page_container` adlı bir kapsayıcı vardır ve silinmez.** Komponentler köke değil
|
|
391
|
+
**onun `children`'ına** eklenir:
|
|
392
|
+
|
|
393
|
+
```jsonc
|
|
394
|
+
{ "op": "add_component", "parent": "page_container", "component": { … } } // ✔
|
|
395
|
+
{ "op": "add_component", "parent": "root", "component": { … } } // ✘ kapsayıcı dışında kalır
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
`page_container` bir `container`'dır (`isWrapper: true`, `isDroppable: true`,
|
|
399
|
+
`isScrollable: true`, `appearance.heightType: 'auto'`). `container`, `form` ve `repeater` de aynı
|
|
400
|
+
şekilde kapsayıcıdır — onların içine de `parent` vererek eklenir.
|
|
401
|
+
|
|
402
|
+
Sayfa düzeyi olaylar (page load vb.) **bu kapsayıcıya** bağlanır:
|
|
403
|
+
`set_event { target: 'page_container', event: 'mount', eventValue: <flow id> }`.
|
|
404
|
+
`page_container`'ın kendi `isLoading` prop'u vardır — sayfa açılış akışında yükleniyor bayrağı olarak
|
|
405
|
+
onu kullan. Bir `container` olduğu için **düzen prop'larını da alır**: çocuklarını ızgara yerine
|
|
406
|
+
akışla (ortalama, sağa yaslama, aralık) dizmek istiyorsan `layout: 'flex'` — Kural 1f-2.
|
|
407
|
+
|
|
408
|
+
**Tek istisna `modal` ve `drawer`:** onlar `page_container`'ın içinde değil, şemanın **kökünde**
|
|
409
|
+
kapsayıcının kardeşi olarak yaşar (qm `[SY] Profil bilgilerim`: `page_container`, `drawer_personel`,
|
|
410
|
+
`mdl_parolayi_guncelle` aynı seviyede). Modal içeriği (kendi formu ve `submit` akışı dahil) modalın
|
|
411
|
+
`children`'ına eklenir. Diğer her komponent kapsayıcıya girer.
|
|
412
|
+
|
|
413
|
+
> `page_create` sayfayı boş şemayla açar; **gateway `page_container`'ı senin için ekler** ve
|
|
414
|
+
> yanıtta `gatewayNormalized` ile bildirir. `page_schema_get` ile geri okuduğunda ilk düğüm odur.
|
|
415
|
+
|
|
416
|
+
**Boyut:** `w` 12'lik ızgaradadır. Yükseklikte belirleyici alan `size.h` değil
|
|
417
|
+
`appearance.heightType`'tır:
|
|
418
|
+
|
|
419
|
+
- **`auto`** — kutunun yüksekliği İÇERİKTEN gelir; Studio tuvalde ölçtüğü yüksekliği
|
|
420
|
+
`blocks[].size.h`'e geri yazar ve kardeşleri aşağı iter (`ResizeObserver` → `shrinkOrExpand`).
|
|
421
|
+
Verdiğin `h` yalnız bir başlangıç tahminidir, kırpma yapmaz.
|
|
422
|
+
- **`fixed`** — kutu tam `size.h * 10px` çizilir ve **taşan içerik KESİLİR**
|
|
423
|
+
(`.draggable__item-component { overflow: hidden }`). Yalnız bilerek sınırlamak istediğinde ver.
|
|
424
|
+
|
|
425
|
+
> **Gateway bunu senin için yazıyor (PAGE-014).** Platform `appearance` verilmemiş gövdede
|
|
426
|
+
> yüksekliği `fixed`e sabitler — Studio ise komponentin katalogdaki `isDefaultAutoHeight`
|
|
427
|
+
> değerine bakar. MCP hattında oluşan tüm sayfalar bu yüzden kırpılmış çiziliyordu; gateway artık
|
|
428
|
+
> yazma yollarında (`page_create` · `page_update(schema)` · `page_schema_patch(add_component)` ·
|
|
429
|
+
> `layout_*(views)` · `component_*(schema)`) her düğüme katalog varsayılanını damgalar ve
|
|
430
|
+
> `gatewayNormalized` ile bildirir. Senin verdiğin `appearance.heightType`'a dokunmaz.
|
|
431
|
+
>
|
|
432
|
+
> Katalogda `isDefaultAutoHeight: false` olan (yani `fixed` damgalanan) tipler: `auth-layout`,
|
|
433
|
+
> `html`, `image`, `layout`, `qrcode`, `table`, `text-editor`, `video`. Diğer her tip `auto`'dur.
|
|
434
|
+
> Bu tiplerde `size.h` gerçek yüksekliktir — göz kararı değil, içeriğe göre hesapla
|
|
435
|
+
> (bir satır ≈ 10px; Kural 1f).
|
|
436
|
+
|
|
437
|
+
## Kural 1d-2 — Sayfanın ve layout'un KENDİ olayları: `mount` · `destroy`
|
|
438
|
+
|
|
439
|
+
Komponent olaylarından AYRI olarak sayfa ve layout kayıtları kendi olay listesini taşır
|
|
440
|
+
(`events: [{ name, value: <flow id> }]`) ve geçerli adlar SABİTTİR: **`mount`**
|
|
441
|
+
(yüklendiğinde) ve **`destroy`** (sayfadan/layouttan ayrılırken — runtime destroy akışlarını
|
|
442
|
+
navigasyondan ÖNCE koşar). Başka ad platformca kabul edilir ama bağ sessizce ölü kalır —
|
|
443
|
+
gateway **PAGE-011 / LAY-004** ile istek gitmeden reddeder.
|
|
444
|
+
|
|
445
|
+
- Bağlama yolu `set_event` DEĞİLDİR (o komponent hedefler): sayfada `page_create` /
|
|
446
|
+
`page_update`'in `events` parametresi, layout'ta `layout_create/update`'in `events`'i.
|
|
447
|
+
- Sayfa açılış akışı için yerleşik desen `page_container.mount`'tur (Kural 1d) — ikisi de
|
|
448
|
+
çalışır; **`destroy` için tek yol buradaki sayfa olayıdır** (interval temizliği, "ayrılmadan
|
|
449
|
+
kaydet" uyarısı gibi işler).
|
|
450
|
+
- Akış kilidi burada da geçerlidir (Kural 3): önce bütün `flow_create`'ler, sonra events bağı.
|
|
451
|
+
- Mevcut bağları `page_get` gösterir (`events[].value` = flow id).
|
|
452
|
+
|
|
453
|
+
## Kural 1e — İKİ tablo komponenti var: `table` ve `datatable`
|
|
454
|
+
|
|
455
|
+
> **`data-table` KALDIRILDI (2026-08-17).** Katalogda böyle bir slug yok; eski bir örnekte ya da
|
|
456
|
+
> eski bir sayfada görürsen `datatable` ya da `table` ile değiştir. Katalogda tablo işi gören
|
|
457
|
+
> başka komponent yoktur — aşağıdaki iki satır listenin tamamıdır.
|
|
458
|
+
|
|
459
|
+
| slug | Olaylar | Ayırt edici prop'lar |
|
|
460
|
+
|---|---|---|
|
|
461
|
+
| `table` | 8: `mount · pagination · filter · sort · select · selectionChange · doubleClick · rowChange` | `rowSelection` (none/single/multi), `selectedRow`/`selectedRows`, `filterMode` (panel/hide/row), `filters`, `showSort`, `sortField`/`sortType`, `isEditable`, `showColumnMenu`, `actions`, `emptyIcon/emptyText/emptyActions`, `style` (root/thead/th/tr/td) |
|
|
462
|
+
| `datatable` | 1: `mount` | `title`, `locale` (tr/en), `tableStyle` (zebra/plain), kolonlarda `isFilterable`/`isSortable`/`colorMap`/`prefix`/`suffix`/`precision`/`thousands`/`dateFormat`/`imageShape`, `filters`/`sortField`/`sortType` (hidden), `actions` |
|
|
463
|
+
|
|
464
|
+
**`datatable`'ın tek olay yayması eksiklik değil, tasarım.** Filtre paneli, sıralama ve sayfalama
|
|
465
|
+
komponentin İÇİNDE: kullanıcı süzdüğünde/sıraladığında/sayfa değiştirdiğinde `datatable` önce
|
|
466
|
+
kendi `filters`/`sortField`/`sortType`/`offset` prop'larını node'a yazar, sonra **`mount`'u
|
|
467
|
+
yeniden tetikler** (`updateQuery` → `emitMount`). Yani tek bir `mount` akışı yazarsın; o akış
|
|
468
|
+
tabloya bağlı uca bu prop'ları parametre olarak geçirir ve dönen satırları geri yazar.
|
|
469
|
+
|
|
470
|
+
**`table`'da her etkileşim AYRI bir olaydır** ve her birini ayrı akışa bağlarsın (`filter`,
|
|
471
|
+
`sort`, `pagination` olayları yükte yeni değerleri de taşır — yukarıdaki olay yükü tablosu).
|
|
472
|
+
|
|
473
|
+
Her ikisinin de süzme/sıralaması sunucu taraflıdır: seçilen filtreler uca `filters` parametresi
|
|
474
|
+
olarak gider ve ancak `enableDynamicFilters: true` olan bir uçta karşılık bulur
|
|
475
|
+
(`platform_guide('microservice')`).
|
|
476
|
+
|
|
477
|
+
**Karar senin, dayatma yok.** Sorular:
|
|
478
|
+
|
|
479
|
+
- *Kullanıcı bu listeyi ekrandan süzecek/sıralayacak mı?* Hayır ise ikisi de olur; en az kavramla
|
|
480
|
+
kurulanı seç ve `mount` akışı yeterlidir.
|
|
481
|
+
- *Süzme/sıralama var mı?* → `datatable` tek akışla kurar (daha az bağlama), `table` her olayı
|
|
482
|
+
ayrı akışa bağlamanı ister ama karşılığında satır seçimi (`rowSelection`), çift tık, hücre içi
|
|
483
|
+
düzenleme (`isEditable`) ve boş-durum aksiyonları verir.
|
|
484
|
+
- *Satır seçimi / çift tıkla forma geçiş / hücre düzenleme gerekiyor mu?* → **`table`**; bunlar
|
|
485
|
+
`datatable`'da yok.
|
|
486
|
+
- *Zengin kolon biçimlendirme (renk haritası, para birimi öneki, tarih formatı, avatar) gerekiyor
|
|
487
|
+
mu?* → **`datatable`**; kolon şeması bunun için tasarlandı.
|
|
488
|
+
|
|
489
|
+
**Bu, katalogdaki her komponent için geçerlidir:** hangi komponenti kullanacağını
|
|
490
|
+
`component_list`/`component_get` ile keşfedip kendin seçersin, rehberdeki örnekler tercih değil
|
|
491
|
+
şablondur.
|
|
492
|
+
|
|
493
|
+
Kaçınılacak tek şey **yarım kurulum**: süzme arayüzünü açıp (`table` + `filterMode:'row'`, ya da
|
|
494
|
+
`datatable` + `isFilterable` kolonlar) arkasına statik filtreli uç bağlamak. Sayfa çalışır
|
|
495
|
+
görünür, kullanıcı süzer, hiçbir şey olmaz. Ya süzme arayüzünü kapat, ya ucu
|
|
496
|
+
`enableDynamicFilters: true` yap ve akışta `filters` parametresini geçir.
|
|
497
|
+
|
|
498
|
+
> Komponenti sonradan değiştirmek pahalıdır: flow'un `setter` adımı hedefi kayıt anında
|
|
499
|
+
> **DesignComponent id**'sine çözer (`StepIterate.component?.Id`), komponenti silip yeniden
|
|
500
|
+
> eklersen elementId aynı olsa bile o bağ kopar ve flow'u yeniden kaydetmen gerekir. Tipi
|
|
501
|
+
> baştan doğru seç.
|
|
502
|
+
|
|
503
|
+
## Kural 1f — Koordinat modeli ve layout-slot yerleşimi
|
|
504
|
+
|
|
505
|
+
Izgara kapsayıcıya görelidir ve **bir satır ≈ 10px**'tir; `w` 12 kolon. Her komponentin
|
|
506
|
+
`blocks[].position` (x,y) değeri **kendi parent'ının** ızgarasında anlamlıdır — sayfanın
|
|
507
|
+
tamamında değil.
|
|
508
|
+
|
|
509
|
+
**Sayfa şeması layout'un `slot`'unun İÇİNE enjekte edilir** (Studio ve runtime aynı mapper'ı
|
|
510
|
+
kullanır: `page.mappers.ts` `mapPageLayoutToPreview` — layout varsa çizilen şema layout'un
|
|
511
|
+
`views`'üdür, sayfanın şeması slot'un `children`'ına konur). Bundan üç kural çıkar:
|
|
512
|
+
|
|
513
|
+
1. **Layout header'ı/sidebar'ı için sayfada boşluk BIRAKMA.** Header slot'un dışındadır;
|
|
514
|
+
"header'ın altından başlayayım" diye ilk komponente y ofseti verirsen boşluk İKİ KEZ
|
|
515
|
+
oluşur — semptom: üstte kocaman boş bant, içerik ancak scroll'la görünür. İçerik
|
|
516
|
+
`page_container` içinde **y=0'dan** başlar.
|
|
517
|
+
2. **`page_container` her zaman (0,0)'dadır.** Studio her kayıtta bunu kendisi sıfırlar
|
|
518
|
+
(`mapPageSchemaRaw`); gateway de `page_create`/`page_update(schema)` yolunda sıfırlar ve
|
|
519
|
+
`gatewayNormalized` ile bildirir. Kayıtlı bir sayfada y≠0 kapsayıcı görürsen düzeltme
|
|
520
|
+
yolu `page_update(schema)`'dır — `move_component` kapsayıcıya uygulanamaz (PAGE-006).
|
|
521
|
+
3. **Kardeş y'lerini bitişik ver.** Çakışma itmesi yalnız AŞAĞI iter, boşluk kapatmaz.
|
|
522
|
+
Gateway şüpheli boşlukları **PAGE-010 (uyarı)** ile yanıtın `layoutWarnings` alanında
|
|
523
|
+
söyler: kapsayıcı içinde ilk komponent y>4'te başlıyorsa ya da ardışık kardeşler arasında
|
|
524
|
+
>12 satır (~120px) dikey bant varsa. Layout `views`'ünde aynı analiz **LAY-003**'tür.
|
|
525
|
+
Bilinçli boşluksa uyarıyı yok say; değilse y'leri toparla.
|
|
526
|
+
|
|
527
|
+
> **Layout `views`'ünde satırlar YIĞILMAZ — yalnız kolonlar çalışır.** Ölçüm (anket projesi,
|
|
528
|
+
> 2026-08-19, 1280×720): layout'un doğrudan çocuklarına `position.y` verildiğinde hepsi
|
|
529
|
+
> **top:0**'da çizilir ve üst üste biner; `x`/`w` ise doğru uygulanır. Ölçülen değerler —
|
|
530
|
+
> `public_topbar` (y:0,h:2) → top 0, **576px**; `public_slot` (y:2,h:78) → **top 0**, 576px:
|
|
531
|
+
> ikisi çakıştı, sayfa içeriği header'ın altında kaldı. `heightType: 'auto'` yapmak yüksekliği
|
|
532
|
+
> düzeltmedi (kap yine 576px kaldı).
|
|
533
|
+
>
|
|
534
|
+
> Çalışan desen **kolon ayrımıdır**: sidebar `{x:0,w:2}` + slot `{x:2,w:10}` → sidebar solda,
|
|
535
|
+
> içerik sağda, çakışma yok (aynı ölçümde doğrulandı). Üstte şerit istiyorsan iki seçenek var:
|
|
536
|
+
> (a) şeridi **sayfanın kendi şemasına** koy (ya da tek `html` ekranıysa şablonun içine), ya da
|
|
537
|
+
> (b) katalogdaki `header`/`sidebar` **layout komponent tipleri**ni kullan — generic `container`
|
|
538
|
+
> yerine onlar için ayrı yerleşim yolu var (bu ikinci yol ölçülmedi).
|
|
539
|
+
>
|
|
540
|
+
> Semptom sözlüğü: layout bağlıyken sayfada **devasa üst boşluk + içeriğin başlıkla çakışması**
|
|
541
|
+
> görüyorsan sebep budur; layout bağını kaldırdığında sayfa düzgün çiziliyorsa teşhis kesinleşir.
|
|
542
|
+
|
|
543
|
+
> **Neden 576px? `states.isWrapper: true` düğüme `min-height: 80vh` bastırıyor.** Renderer
|
|
544
|
+
> `getStyle` içinde wrapper düğümlere koşulsuz `height:100%` + **`min-height:80vh`** +
|
|
545
|
+
> `overflow-y:inherit` yazıyor (`RenderComponent.svelte:134`). 720px'lik pencerede bu tam
|
|
546
|
+
> **576px**'tir — yukarıdaki layout ölçümündeki sayı da, sayfa içinde boş görünen dev
|
|
547
|
+
> kutuların sayısı da budur. Ölçüm (anket-native sayfası, 2026-08-19): `isWrapper:true` bir
|
|
548
|
+
> `container`'ın içinde 36px'lik tek bir `multi-select` varken kutu **576px** çizildi; aynı
|
|
549
|
+
> düğüm `isWrapper:false` yapılınca **36px**'e indi (kart yüksekliği 666px → 126px).
|
|
550
|
+
>
|
|
551
|
+
> Kural: **`isWrapper` yalnız gerçekten sayfa boyu kaplaması gereken kap içindir**
|
|
552
|
+
> (`page_container` böyledir ve öyle kalmalı). Sadece "başlık/çerçeve çizilmesin" diye wrapper
|
|
553
|
+
> yapma — o iş `container` için `style` ile (`background:'transparent'`, `border:'none'`),
|
|
554
|
+
> `form` için `hideBorders`/`hidePadding` prop'larıyla yapılır. **`states` için micro-op
|
|
555
|
+
> YOKTUR:** yanlış `isWrapper`'ı düzeltmenin tek yolu düğümü `remove_component` +
|
|
556
|
+
> `add_component` ile yeniden kurmaktır (sonrasında Kural 2'deki flow yeniden kaydetme adımı
|
|
557
|
+
> ZORUNLU olur).
|
|
558
|
+
|
|
559
|
+
Mobil not: <768px'te ızgara tek kolona düşer (`RenderComponent` media query'si) — y sırası
|
|
560
|
+
dikey sıraya dönüşür, x/w yok sayılır. Yerleşimi masaüstü (breakpoint 1024) bloğuyla kur.
|
|
561
|
+
|
|
562
|
+
## Kural 1f-2 — Kapsayıcı düzeni: `flex` (varsayılan) ↔ `grid`
|
|
563
|
+
|
|
564
|
+
`container` çocuklarını iki şekilde dizebilir ve **`page_container` de bir `container`'dır** —
|
|
565
|
+
aynı prop grubunu o da alır. Grup katalogda tanımlıdır, yani şemadan kurulabilir; ölçüm
|
|
566
|
+
(2026-08-25, `page_schema_validate` kuru koşusu): yedi prop da `valid: true` döndü,
|
|
567
|
+
`layout: 'flexbox'` → `invalid-enum-value`, `flexDirection` → `unknown-prop`
|
|
568
|
+
(doğru adlar `component_get(slug:'container')` çıktısındadır).
|
|
569
|
+
|
|
570
|
+
| Prop | Değerler (varsayılan **kalın**) | Ne yapar |
|
|
571
|
+
|---|---|---|
|
|
572
|
+
| `layout` | **`flex`** · `grid` | `flex`: akış düzeni. `grid`: 12 kolonluk ızgara (Kural 1f). |
|
|
573
|
+
| `direction` | **`row`** · `column` | `flex-direction`; hangi hizalamanın ANA eksen olduğunu belirler. |
|
|
574
|
+
| `horizontalAlign` | **`left`** · `center` · `right` · `stretch` | `row`'da ana eksen (`justify-content`), `column`'da çapraz eksen (`align-items`). |
|
|
575
|
+
| `verticalAlign` | **`top`** · `middle` · `bottom` · `stretch` | `row`'da çapraz eksen, `column`'da ana eksen (rolleri `horizontalAlign` ile takas). |
|
|
576
|
+
| `size` | **`shrink`** · `grow` | Çocuklara `flex: 0 1 auto` / `1 1 auto`. |
|
|
577
|
+
| `gap` | `none` · `small` · **`medium`** · `large` | 0 / 8 / 16 / 32px. |
|
|
578
|
+
| `wrap` | **kapalı** · açık | `flex-wrap`; kapalıyken taşan çocuklar sıkışır, alta inmez. |
|
|
579
|
+
|
|
580
|
+
`stretch` iki eksende farklı anlama gelir: **ana** eksende `space-between` (çocukları uçlara
|
|
581
|
+
dağıtır), **çapraz** eksende gerçekten doldurur.
|
|
582
|
+
|
|
583
|
+
**Varsayılan ≠ renderer davranışı — ikisini karıştırma.** `layout`'un katalog varsayılanı
|
|
584
|
+
2026-08-25'te `grid` → `flex` oldu, ama varsayılan düğüm **kurulurken** props'a yazılır (Studio
|
|
585
|
+
bırakma yolu `definition.defaults`'u kopyalar); renderer varsayılanı bilmez, yalnız düğümün kendi
|
|
586
|
+
prop'una bakar (`isFlexLayout`). İki sonucu var:
|
|
587
|
+
|
|
588
|
+
- **Eski kayıtlar değişmedi:** `layout` prop'u olmayan her kapsayıcı ızgara çizilmeye devam eder.
|
|
589
|
+
- **Sen şema kuruyorsan varsayılana güvenme:** istediğin düzeni `layout`'a AÇIKÇA yaz. Yazmazsan
|
|
590
|
+
ızgara alırsın — Studio'dan sürüklenen kardeş kapsayıcı flex gelirken seninki grid kalır.
|
|
591
|
+
|
|
592
|
+
Gateway'in kendi eklediği kök kapsayıcı (`page_create`'te şemada `page_container` yoksa) bu yüzden
|
|
593
|
+
seti açıkça taşır: `flex` · `row` · `left` · `top` · `shrink` · `medium`. **`row` + `wrap` kapalı
|
|
594
|
+
demek, alt alta düşünülmüş `w: 12` çocukların TEK SATIRA sıkışması demek** — dikey akan bir sayfa
|
|
595
|
+
istiyorsan kapsayıcıya `direction: 'column'` yaz (ya da `layout: 'grid'`e dön).
|
|
596
|
+
|
|
597
|
+
**Ne zaman flex?** Yan yana buton/aksiyon grubu, toolbar, rozet dizisi, ortalanmış giriş kartı,
|
|
598
|
+
"kalanı doldursun" yerleşimleri. Alt alta akan form alanları için ızgara da doğrudur — orada flex
|
|
599
|
+
istiyorsan `direction: 'column'` şart, `row` ile alanlar tek satıra sıkışır.
|
|
600
|
+
|
|
601
|
+
Dört şeyi bilmeden flex kurma:
|
|
602
|
+
|
|
603
|
+
1. **Genişlik yine 12 kolondur — içerik genişliği DEĞİL.** Çocuğun `blocks[].size.w`'si
|
|
604
|
+
`row`'da `flex-basis: w/12`, `column`'da `width: w/12` olur (`gap` payı formülden düşülür,
|
|
605
|
+
böylece `w` toplamı 12 olan satır tam kapanır). Bu bilinçli bir tercih: ızgara ↔ flex geçişi
|
|
606
|
+
kayıpsızdır ve genişlik şemadan ifade edilebilir kalır. `column` + `horizontalAlign: 'stretch'`
|
|
607
|
+
tek istisnadır — orada çocuk tam genişlik olur.
|
|
608
|
+
2. **`position.y` artık yalnız SIRADIR.** Flex çocuklar `y`, sonra `x` sırasına göre dizilir;
|
|
609
|
+
aradaki boşluklar çizilmez. Yine de bitişik ver (0,1,2…): kapsayıcı ileride `grid`'e
|
|
610
|
+
döndürülürse şema geçerli kalsın. Gateway bu yüzden **flex kapsayıcının İÇİNDE PAGE-010
|
|
611
|
+
boşluk uyarısı üretmez** (ızgarada üretmeye devam eder).
|
|
612
|
+
3. **Flex kapsayıcının yüksekliği içeriğine oturur.** `verticalAlign`'ın dağıtacak boşluğu
|
|
613
|
+
olması için `appearance.heightType: 'fixed'` + `size.h` vermelisin. **İstisna
|
|
614
|
+
`page_container`**: `states.isWrapper: true` olduğu için zaten `min-height: 80vh` alır
|
|
615
|
+
(Kural 1f), dolayısıyla dikey hizalama onda kutu boyu vermeden de çalışır.
|
|
616
|
+
4. **`layout: 'grid'` iken diğer altı prop ÖLÜDÜR.** Katalogda tanımlı oldukları için
|
|
617
|
+
doğrulama duvarı susar, renderer da okumaz: ızgara kapsayıcıya `direction: 'column'` yazmak
|
|
618
|
+
sessizce hiçbir şey yapmaz. Gateway bunu **PAGE-015 (uyarı)** ile söyler.
|
|
619
|
+
|
|
620
|
+
```jsonc
|
|
621
|
+
// Ortalanmış giriş kartı — style ile CSS hack'i YOK, kabın kendi düzeni
|
|
622
|
+
{ "op": "replace_prop", "target": "page_container", "prop": "layout", "value": "flex" }
|
|
623
|
+
{ "op": "replace_prop", "target": "page_container", "prop": "direction", "value": "column" }
|
|
624
|
+
{ "op": "replace_prop", "target": "page_container", "prop": "horizontalAlign", "value": "center" } // çapraz eksen → yatay ortalama
|
|
625
|
+
{ "op": "replace_prop", "target": "page_container", "prop": "verticalAlign", "value": "middle" } // ana eksen → dikey ortalama
|
|
626
|
+
{ "op": "replace_prop", "target": "page_container", "prop": "gap", "value": "large" }
|
|
627
|
+
// kart w:4 verilirse %33 genişlikte ve ortada durur (column'da genişlik = w/12)
|
|
628
|
+
|
|
629
|
+
// Sağa yaslı aksiyon şeridi — kabın kendisi w:12, çocuklar sıkışmadan sağa toplanır
|
|
630
|
+
{ "op": "replace_prop", "target": "row_actions", "prop": "layout", "value": "flex" }
|
|
631
|
+
{ "op": "replace_prop", "target": "row_actions", "prop": "horizontalAlign", "value": "right" }
|
|
632
|
+
{ "op": "replace_prop", "target": "row_actions", "prop": "verticalAlign", "value": "middle" }
|
|
633
|
+
{ "op": "replace_prop", "target": "row_actions", "prop": "gap", "value": "small" }
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
> Düzen işi `style` prop'una AİT DEĞİL. Kural 1h'deki `style` reçeteleri tema/görsellik içindir;
|
|
637
|
+
> `display:flex` gibi düzen kurallarını oraya yazmak kabın kendi düzeniyle çakışır (PAGE-014'teki
|
|
638
|
+
> ölçüm: string style ile kart 66px'e çöktü). Yerleşim bu prop grubuyla, tema `style` ile kurulur.
|
|
639
|
+
|
|
640
|
+
## Kural 1g — Dinamik form (satır sayısı ve alan tipi veriden gelen ekran): `repeater`
|
|
641
|
+
|
|
642
|
+
"N tane soru, her birinin cevap tipi farklı" gibi ekranlar için `html` kaçış kapısına gerek
|
|
643
|
+
YOK — `repeater` (katalog adı *List View*) bunun hazır cevabıdır. **Tam gövde (iki akış, satır
|
|
644
|
+
şekli, bağ listesi, doğrulama adımları): `ngapps://examples/page-dynamic-form`.** Ölçülmüş çalışan desen
|
|
645
|
+
(anket-native sayfası, 2026-08-19; 4 soru: tekil seçim / çoklu seçim / evet-hayır / serbest
|
|
646
|
+
metin → tek gönderimde kaydedildi):
|
|
647
|
+
|
|
648
|
+
```
|
|
649
|
+
q_form (form)
|
|
650
|
+
└─ q_list (repeater) rows = [{id, no, text, help, required, kind,
|
|
651
|
+
└─ q_card (container) is_single, is_multi, is_text, is_yesno,
|
|
652
|
+
├─ q_head (text) options:[{label,value}], answer}]
|
|
653
|
+
├─ q_help (text)
|
|
654
|
+
├─ a_choice (radio) isHidden: {{ !(item.is_single || item.is_yesno) }}
|
|
655
|
+
├─ a_text (textarea) isHidden: {{ !item.is_text }}
|
|
656
|
+
└─ box_multi (container) isHidden: {{ !item.is_multi }}
|
|
657
|
+
└─ a_multi (multi-select)
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
- **Şablon `children`'dır.** Repeater `rows` dizisindeki her satır için `children` ağacını
|
|
661
|
+
yineler; satır bağlamı çocuklara `item` (eşanlamlısı `Row`, sırası `RowIndex`) olarak iner
|
|
662
|
+
ve **iç içe her seviyeye** taşınır (`box_multi` içindeki `a_multi` de `item`'ı görür).
|
|
663
|
+
- **İki yönlü bağ vardır ve flow gerektirmez.** Bir çocuk komponentin prop'u kod ifadesiyse ve
|
|
664
|
+
ref'i **tam olarak** `{{ item.<alan> }}` kalıbına uyuyorsa, kullanıcı o alanı değiştirdiğinde
|
|
665
|
+
runtime değeri `rows.data[<index>].<alan>`'a geri yazar
|
|
666
|
+
(`repeater.handler.ts` → `handleRepeaterItemPropertySet`, regex `^\{\{\s*item\.(.+?)\s*\}\}$`).
|
|
667
|
+
Yani `value: {type:'code', ref:'{{ item.answer }}'}` yazmak "cevabı satıra kaydet" demektir.
|
|
668
|
+
**Kalıp bozulursa geri yazma sessizce çalışmaz:** `{{ item.answer || '' }}` ya da
|
|
669
|
+
`{{ item["answer"] }}` regex'e uymaz — okur ama yazmaz.
|
|
670
|
+
- **Toplu okuma flow'da `<repeaterAdı>.rows`'tur.** `microservice` adımının `value.type:'code'`
|
|
671
|
+
parametresinde ve `condition` adımının `expression`'ında komponentlere adıyla erişilir, yani
|
|
672
|
+
`JSON.stringify((q_list.rows || []).map(…))` doğrudan çalışır. Kod bağlamında satırlar
|
|
673
|
+
**ham** gelir (Prop sarımı `mapSchemaPropToRawProp` ile her seviyede açılır); `machine.getNode`
|
|
674
|
+
ile bakarsan `{data,type}` sarılı görünür — ikisini karıştırma.
|
|
675
|
+
- **Satır kimliği:** `getRepeaterRowId` sırayla `row._id` / `row.id` / index'e bakar. Satırlara
|
|
676
|
+
**kalıcı bir `id` ver**; yoksa index'e düşer ve liste değişince girilen değerler kayar.
|
|
677
|
+
|
|
678
|
+
**Satır tipine göre FARKLI alan göstermek — aynı ızgara hücresi + `isHidden`.** Dört varyantı
|
|
679
|
+
alt alta dizersen gizli olanların satırları boş kalır (gizli düğüm hiç çizilmez ama grid
|
|
680
|
+
koordinatı sabittir → dev boşluk). Doğrusu **hepsini aynı `position`'a koymaktır**; zaten aynı
|
|
681
|
+
anda yalnız biri görünür. Bu güvenlidir çünkü **`page_schema_patch` çakışma normalizasyonu
|
|
682
|
+
UYGULAMAZ** — koordinatları aynen kaydeder (aşağı itme yalnız `page_create` / `page_update`
|
|
683
|
+
/ `layout_update` yolundadır, Kural 1). Aynı sebeple bu deseni komple şema yazarak kuramazsın.
|
|
684
|
+
|
|
685
|
+
> **`isHidden` her komponentte YOK.** Renderer `{#if !props.isHidden}` ile *her* düğümü
|
|
686
|
+
> geçiriyor, ama prop sözlüğünde tanımlı değilse doğrulama duvarı `unknown-prop` ile reddeder.
|
|
687
|
+
> Ölçüm: `radio`, `textarea`, `container`, `form`, `button` **var**; `text` ve `multi-select`
|
|
688
|
+
> **yok**. Çözüm: gizlenecek komponenti `isHidden`'ı olan bir `container`'a sar ve koşulu
|
|
689
|
+
> sarmalayıcıya bağla (yukarıdaki `box_multi`). Sarmalayıcıyı **`isWrapper:false`** kur
|
|
690
|
+
> (Kural 1f: wrapper = `min-height:80vh`) ve çerçevesini `style` ile kapat:
|
|
691
|
+
> `{ background:'transparent', border:'none', '&__body': { padding:'0px' } }`.
|
|
692
|
+
> `appearance.isHidden` diye bir kaçış yolu YOKTUR — runtime onu hiç okumaz.
|
|
693
|
+
|
|
694
|
+
> **Satır BAŞINA doğrulama kurulamaz.** `validations` komponentin kendisindedir, tüm satırlar
|
|
695
|
+
> onu paylaşır — "3. soru zorunlu, 4. soru değil" şemayla ifade edilemez. Zorunluluk satır
|
|
696
|
+
> verisinde taşınır (`required`) ve gönderim akışının ilk `condition` adımında denetlenir:
|
|
697
|
+
> `(q_list.rows||[]).filter(r => r.required && (r.answer==='' || (Array.isArray(r.answer) && !r.answer.length))).length > 0`.
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
> **⚠ İÇ İÇE REPEATER YAZMA İÇİN KULLANILAMAZ (ölçüldü, 2026-08-19).** Repeater içinde ikinci bir
|
|
701
|
+
> repeater (`rows: {{ item.options }}`) **okumada çalışır** — dış satır başına doğru sayıda iç satır
|
|
702
|
+
> çizilir, değerler doğru gelir. Ama:
|
|
703
|
+
> - **Geri yazma ölü:** iç girdiyi değiştirdiğinde dış satırın alanı DEĞİŞMEZ
|
|
704
|
+
> (`p_outer.rows.data[0].options[0].label` eski kaldı) ve iç repeater'ın `_repeaterValues`'ü
|
|
705
|
+
> `null`. Sebep yapısal: şemada iç repeater **tek bir düğümdür**, dış satır başına ayrı bir düğüm
|
|
706
|
+
> yoktur; `_repeaterValues` anahtarı iç satır id'sine göredir ve dış satırlar arasında çakışır.
|
|
707
|
+
> - **DOM bozuluyor:** tek bir düzenlemeden sonra girdi sayısı 3 → **19**'a çıktı, değerler karıştı.
|
|
708
|
+
> - Ekranda sürekli `Expression error: Cannot read properties of null (reading '0')` toast'ı çıkıyor
|
|
709
|
+
> (iç repeater bir kez `item` olmadan da değerlendiriliyor).
|
|
710
|
+
>
|
|
711
|
+
> **Gateway bunu `PAGE-013` ile istek gitmeden reddediyor** (`page_create` / `page_update(schema)` ve
|
|
712
|
+
> `page_schema_patch`'te op'lar UYGULANDIKTAN SONRAKİ ağaca bakarak).
|
|
713
|
+
>
|
|
714
|
+
> **Yapılacak:** satır içi listeyi tek bir `textarea`'da **satır başına bir öğe** olarak topla
|
|
715
|
+
> (`options_text`), kaydederken `split('\n')`'le böl. Ölçülmüş çalışan sürüm: anket editörü
|
|
716
|
+
> (`anket-admin/anket-duzenle`). Gerçekten satır satır düzenleme gerekiyorsa iç listeyi AYRI bir
|
|
717
|
+
> sayfada/modalda yönet — iç içe repeater bunu karşılamıyor.
|
|
718
|
+
|
|
719
|
+
> **Repeater satırını akıştan silmek/eklemek:** repeater ÇOCUĞUNDAKİ bir butonun click akışında
|
|
720
|
+
> `RowIndex` doğru satırı verir, ve hedef repeater'ın KENDİSİ olduğu için `rows`'a yazmak serbesttir
|
|
721
|
+
> (`isRepeaterSelf` dalı satır-değeri yoluna girmez). Ölçülmüş desen:
|
|
722
|
+
> `q_list.rows = (q_list.rows || []).filter(function(r, i){ return i !== RowIndex; })`.
|
|
723
|
+
> Ekleme aynı şekilde `.slice()` + `push` ile yapılır; satır numarasını ekledikten sonra yeniden ata.
|
|
724
|
+
|
|
725
|
+
**Gönderimi saklamak:** satır başına normalize kayıt yerine tek `TEXT` kolonda anlık görüntü
|
|
726
|
+
(`answers_json`) tutmak bu desende en pratik yoldur — yazarken `JSON.stringify`, okurken platform
|
|
727
|
+
kendisi nesneye çevirir; Studio tablosunda `[object Object]` görünmesi normaldir ve veri sağlamdır
|
|
728
|
+
(`platform_guide('entity')` → "TEXT kolonunda JSON"). Ekranın görsel tarafı için **Kural 1h**:
|
|
729
|
+
tema `--ng-*` değişkenleriyle tek yerden çevrilir, `html` kaçış kapısına gerek kalmaz.
|
|
730
|
+
|
|
731
|
+
## Kural 1h — Tasarım: `style` prop'u ve tema değişkenleri
|
|
732
|
+
|
|
733
|
+
Her komponentin `style` prop'u vardır ve emotion `css()` bloğudur: iç seçiciler, pseudo-class'lar
|
|
734
|
+
ve **CSS değişkeni tanımı** çalışır, üretilen sınıf komponente scope'ludur. Ekranı "modern"
|
|
735
|
+
yapmanın yolu budur — ölçülmüş kurallar (anket-doldur, 2026-08-19):
|
|
736
|
+
|
|
737
|
+
> Bu kural TEMA/GÖRSELLİK içindir. Çocukların dizilimi (ortalama, yaslama, aralık, "kalanı
|
|
738
|
+
> doldur") `style` ile değil kapsayıcının kendi `layout` prop grubuyla kurulur — Kural 1f-2.
|
|
739
|
+
|
|
740
|
+
**1. Temayı tek yerden çevir: `page_container.style` içinde `--ng-*` değişkenlerini ez.**
|
|
741
|
+
Runtime bütün renk/tipografi/form tokenlarını CSS değişkeni olarak basar
|
|
742
|
+
(`Theme.svelte`: `--ng-accent-primary`, `--ng-accent-hover`, `--ng-background-surface`,
|
|
743
|
+
`--ng-border-primary`, `--ng-rounded`, `--ng-font`, `--ng-form-border-focus`,
|
|
744
|
+
`--ng-form-radius-md`, `--ng-form-height-md`…). Bunları kök kapsayıcının `style`'ında yeniden
|
|
745
|
+
tanımlarsan **sayfadaki her komponente miras kalır**:
|
|
746
|
+
|
|
747
|
+
```jsonc
|
|
748
|
+
{ "op": "replace_prop", "target": "page_container", "prop": "style", "value": {
|
|
749
|
+
"--ng-accent-primary": "#4F46E5", "--ng-accent-hover": "#4338CA",
|
|
750
|
+
"--ng-form-border-focus": "#4F46E5",
|
|
751
|
+
"background": "#F1F5F9",
|
|
752
|
+
"& > .ng-container__body > .ng-container__slot": {
|
|
753
|
+
"maxWidth": "720px", "margin": "0 auto", "padding": "48px 20px 80px", "height": "auto" } } }
|
|
754
|
+
```
|
|
755
|
+
Ölçüldü: butonun dolgusu tek satırda `rgb(79,70,229)`'a döndü (buton stilini hiç yazmadan) ve
|
|
756
|
+
içerik sütunu 720px'te ortalandı. **İçeriği ortalamak için `.ng-container__slot`'u hedefle** —
|
|
757
|
+
`.ng-app__node-children`'ın `padding`'i renderer tarafından SATIR İÇİ yazılır, sınıfla ezilmez.
|
|
758
|
+
|
|
759
|
+
**2. Alt parça seçicileri `&__parça` şeklindedir ve komponent markup'ıyla eşleşir.** Komponent
|
|
760
|
+
kendi elemanlarına `class="{emotionSınıfı}__parça"` basar; `style` içinde `"&__item"` yazınca
|
|
761
|
+
emotion `.css-xxx__item` üretir ve tutar. Hangi parçaların olduğu `component_get`'in
|
|
762
|
+
`style` prop'unun alt listesindedir (`radio` → `&__items · &__item · &__item-input ·
|
|
763
|
+
&__item-text · &__label · &__description · &__error`). Pseudo-class, `::before/::after`,
|
|
764
|
+
kardeş ve iç seçiciler de yazılabilir: `"&__item:hover &__item-text"`,
|
|
765
|
+
`"&__item-input:checked + &__item-text"`, `"&__item-text::before"`.
|
|
766
|
+
|
|
767
|
+
**3. ⚠ Platform `style` anahtarlarının SIRASINI korumaz.** Prop sözlüğü `Dictionary` olarak
|
|
768
|
+
saklanıp yeniden serileştiriliyor; `appliedPatch` çıktısında `ref` ile `data` anahtar sırasının
|
|
769
|
+
farklı olduğunu görebilirsin. Sonuç: **aynı özgüllükteki iki kendi kuralın arasında "sonra yazan
|
|
770
|
+
kazanır" GARANTİ DEĞİLDİR.** Ölçüm: `&__item` ile `&__item:has(...)` arasında yalnız bazı
|
|
771
|
+
bildirimler geçti. Çözüm — durum kurallarını **özgüllükle** ayır
|
|
772
|
+
(`&__item-input:checked + &__item-text` = 0,2,0 > `&__item-text` = 0,1,0) ya da kritik
|
|
773
|
+
bildirimlere `!important` koy.
|
|
774
|
+
|
|
775
|
+
**4. Komponentin kendi boyut kuralları seninkinden ÖZGÜLDÜR.** `radio`/`textinput` vb.
|
|
776
|
+
`--sm/--md/--lg` modifier'ları `.css-x--md .css-x__item-text { padding-left: … }` gibi (0,2,0)
|
|
777
|
+
kurallar yazar; senin `&__item-text` kuralın (0,1,0) bunu ezemez. Ölçüm: `padding` verdim,
|
|
778
|
+
`padding-left` 22.4px kaldı (dot ile metin çakıştı); `!important` ile 42px'e oturdu. Kural:
|
|
779
|
+
komponentin varsayılanının üstüne yazarken ya `&--md &__parça` yaz ya da `!important` kullan.
|
|
780
|
+
|
|
781
|
+
**5. Gizlenecek/koşullu görsel durumlarda `:has()`'e bel bağlama.** `:has(input:checked)`
|
|
782
|
+
seçicisi doğru eşleşiyor (`element.matches()` true) ama işaretleme çalışma anında değiştiğinde
|
|
783
|
+
tarayıcı üst elemanı yeniden stillendirmeyebiliyor — ölçümde yalnız kalıtılan `color` güncellendi,
|
|
784
|
+
`background`/`border`/`box-shadow` eski kaldı (aynı bayatlama platformun KENDİ radyo noktasında da
|
|
785
|
+
görüldü: `input:checked + span::after { opacity: 1 }` uygulanmadı). Sağlam yol kardeş seçicidir:
|
|
786
|
+
işareti taşıyan input'un **sonraki kardeşini** boya (`&__item-input:checked + &__item-text`).
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
### 1h-2 — Tema değişkeni envanteri (`Theme.svelte`, tamamı)
|
|
790
|
+
|
|
791
|
+
Bunlar **runtime'da her sayfada tanımlıdır**; `style` içinde yeniden tanımlayarak eziyorsun.
|
|
792
|
+
Varsayılan palet `white.ts` (accent `#0052CC`, border `#dfe1e6`, text `#262626`) — yani hiçbir şey
|
|
793
|
+
yapmazsan ekran "platform mavisi" görünür; marka rengine çevirmek tek satırdır.
|
|
794
|
+
|
|
795
|
+
| Küme | Değişkenler |
|
|
796
|
+
|---|---|
|
|
797
|
+
| Vurgu | `--ng-accent-primary` · `--ng-accent-hover` · `--ng-accent-muted` · `--ng-accent-subtle` |
|
|
798
|
+
| Zemin | `--ng-background-canvas` · `--ng-background-surface` · `--ng-background-surface-alt` · `--ng-background-readonly` · `--ng-background-disabled` · `--ng-background-overlay` |
|
|
799
|
+
| Metin | `--ng-text-primary` · `--ng-text-secondary` · `--ng-text-disabled` |
|
|
800
|
+
| Çerçeve / köşe / gölge | `--ng-border-primary` · `--ng-border-secondary` · `--ng-rounded` · `--ng-shadow-none/sm/md/lg` |
|
|
801
|
+
| Tipografi | `--ng-font` · `--ng-font-size` · `--ng-font-weight` |
|
|
802
|
+
| Boşluk | `--ng-spacing` · `--ng-spacing-small` · `--ng-spacing-large` |
|
|
803
|
+
| Durum | `--ng-status-success` · `--ng-status-warning` · `--ng-status-error` · `--ng-status-info` |
|
|
804
|
+
| Form (girdilerin tamamı) | `--ng-form-color` / `-disabled` / `-placeholder` / `-invalid` · `--ng-form-background` / `-disabled` / `-readonly` · `--ng-form-border` / `-focus` / `-disabled` / `-invalid` · `--ng-form-radius-sm/md/lg` · `--ng-form-height-sm/md/lg` · `--ng-form-padding-x-sm/md/lg` · `--ng-form-font-family` · `--ng-form-font-size-sm/md/lg` · `--ng-form-label-color` / `-font-weight` / `-font-size-*` / `-margin-bottom-*` · `--ng-form-default-size` |
|
|
805
|
+
|
|
806
|
+
**Önce token'ı ez, sonra komponent stiline in.** `--ng-form-radius-md`'yi 10px yapmak sayfadaki
|
|
807
|
+
BÜTÜN girdileri yuvarlar; aynı şeyi komponent komponent `&__input { borderRadius }` yazarak
|
|
808
|
+
yapmak hem uzun sürer hem de yeni komponent eklendiğinde tutarsızlık üretir.
|
|
809
|
+
|
|
810
|
+
### 1h-3 — Ölçülmüş hazır bloklar (anket ekranından, olduğu gibi kullanılabilir)
|
|
811
|
+
|
|
812
|
+
```jsonc
|
|
813
|
+
// 1) Sayfa kabuğu — tema + ortalanmış içerik sütunu [page_container.style]
|
|
814
|
+
{ "--ng-accent-primary": "#4F46E5", "--ng-accent-hover": "#4338CA",
|
|
815
|
+
"--ng-form-border-focus": "#4F46E5",
|
|
816
|
+
"background": "#F1F5F9",
|
|
817
|
+
"fontFamily": "'Inter', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif",
|
|
818
|
+
"& > .ng-container__body": { "overflowY": "visible" },
|
|
819
|
+
"& > .ng-container__body > .ng-container__slot": {
|
|
820
|
+
"maxWidth": "720px", "margin": "0 auto", "padding": "48px 20px 80px", "height": "auto" } }
|
|
821
|
+
|
|
822
|
+
// 2) Kart — beyaz yüzey, ince çerçeve, hover'da yükselen gölge [container.style, isWrapper:false]
|
|
823
|
+
{ "background": "#FFFFFF", "border": "1px solid #E2E8F0", "borderRadius": "16px",
|
|
824
|
+
"boxShadow": "0 1px 2px rgba(15, 23, 42, 0.04)",
|
|
825
|
+
"transition": "border-color 140ms ease, box-shadow 140ms ease",
|
|
826
|
+
"&:hover": { "borderColor": "#CBD5E1", "boxShadow": "0 6px 16px rgba(15, 23, 42, 0.07)" },
|
|
827
|
+
"&__body": { "padding": "22px 24px 24px", "overflowY": "visible", "height": "auto" } }
|
|
828
|
+
|
|
829
|
+
// 3) Radyo → tıklanabilir seçenek satırları (46px hedef, seçilince indigo) [radio.style]
|
|
830
|
+
// padding/lineHeight'ta !important ŞART: komponentin --md kuralı daha özgül (madde 4).
|
|
831
|
+
{ "marginTop": "16px",
|
|
832
|
+
"&__items": { "gap": "8px" },
|
|
833
|
+
"&__item": { "display": "block", "cursor": "pointer" },
|
|
834
|
+
"&__item-text": { "display": "block", "border": "1px solid #E2E8F0", "borderRadius": "10px",
|
|
835
|
+
"padding": "12px 14px 12px 42px !important", "lineHeight": "1.45 !important",
|
|
836
|
+
"background": "#FFFFFF", "fontSize": "15px", "color": "#334155",
|
|
837
|
+
"transition": "border-color 120ms ease, background-color 120ms ease, box-shadow 120ms ease" },
|
|
838
|
+
"&__item-text::before": { "left": "15px" },
|
|
839
|
+
"&__item-text::after": { "left": "calc(15px + (var(--ng-form-font-size-md) * 1.1) / 2 + 1px)" },
|
|
840
|
+
"&__item:hover &__item-text": { "borderColor": "#C7D2FE", "background": "#F8FAFF" },
|
|
841
|
+
"&__item-input:checked + &__item-text": { "borderColor": "#4F46E5", "background": "#EEF2FF",
|
|
842
|
+
"boxShadow": "inset 0 0 0 1px #4F46E5", "color": "#1E1B4B", "fontWeight": "500" } }
|
|
843
|
+
|
|
844
|
+
// 4) Metin alanı — odak halkası [textarea.style]
|
|
845
|
+
{ "marginTop": "16px",
|
|
846
|
+
"&__input": { "borderRadius": "10px", "border": "1px solid #E2E8F0", "padding": "12px 14px",
|
|
847
|
+
"fontSize": "15px", "lineHeight": "1.6", "color": "#0F172A", "background": "#FFFFFF",
|
|
848
|
+
"transition": "border-color 120ms ease, box-shadow 120ms ease" },
|
|
849
|
+
"&__input:focus": { "borderColor": "#4F46E5", "boxShadow": "0 0 0 3px rgba(79, 70, 229, 0.12)",
|
|
850
|
+
"outline": "none" } }
|
|
851
|
+
|
|
852
|
+
// 5) Çoklu seçim — dış sarmalayıcıya yazılır, iç kutu iner seçiciyle bulunur [multi-select.style]
|
|
853
|
+
{ "& .ng-multiselect": { "borderRadius": "10px", "border": "1px solid #E2E8F0",
|
|
854
|
+
"minHeight": "44px", "background": "#FFFFFF" },
|
|
855
|
+
"& .ng-multiselect:focus-within": { "borderColor": "#4F46E5",
|
|
856
|
+
"boxShadow": "0 0 0 3px rgba(79, 70, 229, 0.12)" } }
|
|
857
|
+
|
|
858
|
+
// 6) Görünmez-kalabilen hata şeridi [container.style + isHidden akıştan çevrilir]
|
|
859
|
+
{ "background": "#FEF2F2", "border": "1px solid #FECACA", "borderRadius": "10px",
|
|
860
|
+
"marginTop": "20px",
|
|
861
|
+
"&__body": { "padding": "12px 14px", "overflowY": "visible", "height": "auto" } }
|
|
862
|
+
|
|
863
|
+
// 7) Birincil buton — dolgu rengi token'dan gelir, burada sadece ölçü [button.style]
|
|
864
|
+
{ "marginTop": "24px", "height": "48px", "borderRadius": "10px", "fontSize": "15px",
|
|
865
|
+
"fontWeight": "600", "paddingLeft": "30px", "paddingRight": "30px",
|
|
866
|
+
"boxShadow": "0 1px 2px rgba(15, 23, 42, 0.08)" }
|
|
867
|
+
```
|
|
868
|
+
|
|
869
|
+
Ölçülen sonuç (1280×720): gövde 1292px, kartlar 680px genişlik / 14px ara, seçenek satırları
|
|
870
|
+
46px, buton 48px; 375px'te yatay taşma yok, seçenekler 285px. Boş `text` komponenti ~24px yer
|
|
871
|
+
kaplar — yardım metni opsiyonelse bunu hesaba kat.
|
|
872
|
+
|
|
873
|
+
> **Erişilebilirlik notu:** seçenek satırını `label`'ın İÇİNDEKİ `span`'e boyamak tıklama
|
|
874
|
+
> alanını daraltmaz (`label` zaten sarmalayıcıdır) ama **odak halkasını da taşımanız gerekir** —
|
|
875
|
+
> `input` `display:none` olduğu için klavye odağı görünmez. Klavyeyle gezilecek ekranlarda
|
|
876
|
+
> `&__item-input:focus-visible + &__item-text` için de bir dış çizgi tanımlayın.
|
|
877
|
+
|
|
878
|
+
## Kural 2 — Şema değişikliği `page_update` ile YAPILMAZ
|
|
879
|
+
|
|
880
|
+
İki ayrı yazma yolu vardır ve karıştırılmaz:
|
|
881
|
+
|
|
882
|
+
| Değişiklik | Tool | Neden |
|
|
883
|
+
|---|---|---|
|
|
884
|
+
| Komponent ekle/sil/taşı, prop, binding, event | `page_schema_patch` | Doğrulama duvarı + açık Studio canvas'ına SignalR canlı yayını + eşzamanlı yazar kilidi yalnız bu yolda çalışır |
|
|
885
|
+
| Ad, slug, browserTitle, layout, roller, middleware | `page_update` | Üstbilgi alanları micro-op sözlüğünde yok |
|
|
886
|
+
|
|
887
|
+
Platformun `PUT /pages/...` ucu **replace**'tir: gönderilmeyen `schema`/`events`/`roleIds`
|
|
888
|
+
alanları silinir. Gateway bu yüzden `page_update`'te sayfayı önce okur, yalnız verdiğin
|
|
889
|
+
alanları değiştirip tam gövdeyi geri yazar (`layout_update` ve `component_update` de aynı).
|
|
890
|
+
Yine de `schema` parametresini elle doldurursan okuduğun hâli değil, **verdiğin liste** yazılır.
|
|
891
|
+
|
|
892
|
+
### Atomiklik: 500 KB'lık şemada tek komponente dokunmak
|
|
893
|
+
|
|
894
|
+
`page_schema_patch` şemanın tamamını göndermez — yalnız op'ları gönderir. Tam gövde ne LLM
|
|
895
|
+
bağlamına girer ne de tel üzerinden geçer; şemayı platform kendi belleğinde okur, op'ları uygular
|
|
896
|
+
ve geri yazar. Sözleşme (`PatchPageSchemaCommandHandler`):
|
|
897
|
+
|
|
898
|
+
| Aşama | Ne yapar |
|
|
899
|
+
|---|---|
|
|
900
|
+
| Kilit | Sayfa başına 60 sn'lik dağıtık kilit; başka bir aktör yazıyorsa istek `PageSchemaLockedException` ile döner (409) |
|
|
901
|
+
| Sürüm | `baseVersion` verdiysen `UpdatedAt.Ticks` ile karşılaştırılır; tutmazsa `PageSchemaVersionConflictException` (409) ve **hiçbir op uygulanmaz** |
|
|
902
|
+
| Uygulama | Her op hedefini ağaçta arar (`Find` → JSON Pointer) ve **yalnız o düğüme** dokunur; kardeşler, üst düğümler ve diğer alt ağaçlar bit düzeyinde korunur |
|
|
903
|
+
| Hep-ya-hiç | Op'lar önce bellekte uygulanır; **tek bir hata** (bilinmeyen op, bulunamayan hedef, tekrar eden elementId, doğrulama) tüm komutu düşürür — kısmi yazma yoktur |
|
|
904
|
+
| Doğrulama | Tam şema `PageSchemaValidator`'dan geçer (422 + alan bazlı `errors`) |
|
|
905
|
+
| Yazma | Tek transaction (`ITenantContextTransactionalCommand`) |
|
|
906
|
+
| Yayın | Açık Studio canvas'larına SignalR ile `page-schema-patched` |
|
|
907
|
+
|
|
908
|
+
Yanıt üç alan döner: `applied` (uygulanan op sayısı), **`appliedPatch`** (her op'un şemada tam
|
|
909
|
+
olarak hangi JSON Pointer'a ne yaptığı — RFC 6902 biçiminde; "gerçekten yalnız o komponente mi
|
|
910
|
+
dokunuldu" sorusunun kanıtı) ve **`newVersion`** (`UpdatedAt.Ticks`).
|
|
911
|
+
|
|
912
|
+
**Sürüm kontrolü platformda var ama gateway bugün KULLANMIYOR.** Platform gövdede `baseVersion`
|
|
913
|
+
(long tick) kabul eder ve tutmazsa 409 verir; `page_state.baseVersion` bu değeri döner.
|
|
914
|
+
`page_schema_patch` bu alanı geçirmez — bayat plan koruması gateway tarafında
|
|
915
|
+
`expectedUpdatedAt` ile yapılır: sayfa okunur, `audit.updatedAt` karşılaştırılır, tutmazsa istek
|
|
916
|
+
hiç gönderilmez. Bu kontrol **iki çağrı arasına başka bir yazar girerse yakalamaz**; gerçek
|
|
917
|
+
korumayı isteyen ham HTTP ile `baseVersion` göndermelidir.
|
|
918
|
+
|
|
919
|
+
> **Büyük şemada çalışma sırası:** `mode:'summary'` ile ağacı çıkar (props yok, 500 KB'lık sayfada
|
|
920
|
+
> bile küçük) → hedefin `elementId`'sini al → gerekiyorsa `mode:'subtree'` ile yalnız o dalı oku →
|
|
921
|
+
> op'ları `page_schema_validate` ile kuru koştur → `page_schema_patch`. `mode:'full'` yalnız
|
|
922
|
+
> gerçekten tüm şema gerektiğinde çağrılır.
|
|
923
|
+
|
|
924
|
+
> **`remove_component`'in yan etkisi — tek istisna.** Sayfa kaydedilirken artık hiçbir komponent
|
|
925
|
+
> olayının, prop'unun ya da sayfa olayının referans vermediği akışlar platformca **kalıcı olarak
|
|
926
|
+
> silinir** (`PageService.UpdateAsync` → `flowRepository.DeleteRangeAsync`). Yani akış bağlı bir
|
|
927
|
+
> komponenti silmek, o akış başka yerde kullanılmıyorsa akış kaydını da götürür. Gateway bunu
|
|
928
|
+
> yanıtta `flowWarnings` ile bildirir; akışı korumak istiyorsan silmeden önce başka bir komponente
|
|
929
|
+
> `set_event` ile bağla ya da `flow_get` ile gövdesini yedekle. Ayrıntılı analiz:
|
|
930
|
+
> `PatchPageSchemaCommandHandler` (backend) — kilit, sürüm ve hep-ya-hiç davranışının kaynağı.
|
|
931
|
+
|
|
932
|
+
## Kural 3 — Flow yazımı barındırıcıyı da günceller
|
|
933
|
+
|
|
934
|
+
Platform `flow` uçlarında gövdede hem akışı hem barındırıcının tam güncelleme komutunu ister
|
|
935
|
+
(`{ flow, page | layout | component }`) — şemadaki `events[].value` = flow id bağı tek işlemde
|
|
936
|
+
tutarlı kalsın diye. **Silme bile gövdelidir.** Gateway barındırıcıyı okuyup gövdeye kendisi
|
|
937
|
+
koyar; sen yalnız akışı verirsin.
|
|
938
|
+
|
|
939
|
+
Akışı bir komponente bağlamak ayrı adımdır:
|
|
940
|
+
|
|
941
|
+
```
|
|
942
|
+
flow_create(host:'page', hostId:<pageId>, name:'Save order', slug:'save-order', steps:[…])
|
|
943
|
+
page_schema_patch(ops:[{ op:'set_event', target:'button_1', event:'click', eventValue:<flow id> }])
|
|
944
|
+
```
|
|
945
|
+
|
|
946
|
+
> Olay adı `component_get`'in `events` listesinden alınır ve **`on` öneki almaz**: buton `click`,
|
|
947
|
+
> form `submit`, tablo `mount`/`select`/`sort`. `onClick` yazarsan platform 200 döner ama bağ
|
|
948
|
+
> ölüdür — gateway bunu PAGE-008 ile istek gitmeden reddeder.
|
|
949
|
+
|
|
950
|
+
`flow_create`'e `flowId` verirsen id'yi önceden bilir, iki adımı tek planda kurabilirsin.
|
|
951
|
+
|
|
952
|
+
### Sayfada bağlı akış varken YENİ akış eklenemez — ve çözümü
|
|
953
|
+
|
|
954
|
+
Bir sayfanın herhangi bir olayı zaten bir akışa bağlıysa, o sayfaya ikinci bir `flow_create`
|
|
955
|
+
FK ihlaliyle reddedilir:
|
|
956
|
+
|
|
957
|
+
```
|
|
958
|
+
Geçersiz istek: This record is in use. Please remove its relations first.
|
|
959
|
+
```
|
|
960
|
+
|
|
961
|
+
Bu yüzden **kuruluş sırası bağlayıcıdır** — önce şema, sonra **bütün** akışlar, en son **tek**
|
|
962
|
+
patch'te bütün `set_event`'ler:
|
|
963
|
+
|
|
964
|
+
```
|
|
965
|
+
page_create → page_schema_patch (tüm add_component)
|
|
966
|
+
→ TÜM flow_create'ler
|
|
967
|
+
→ TEK page_schema_patch (bütün set_event'ler)
|
|
968
|
+
```
|
|
969
|
+
|
|
970
|
+
Sonradan akış eklemen gerekirse sayfayı baştan kurmana gerek yok — **bağı geçici olarak çöz**:
|
|
971
|
+
|
|
972
|
+
```jsonc
|
|
973
|
+
1) page_schema_patch [{ "op":"set_event", "target":"page_container", "event":"mount", "eventValue": null }]
|
|
974
|
+
2) flow_create × N
|
|
975
|
+
3) page_schema_patch [ bütün set_event'ler tek seferde ]
|
|
976
|
+
```
|
|
977
|
+
|
|
978
|
+
`eventValue: null` olay kaydını bırakıp değerini boşaltır ve kilidi açar.
|
|
979
|
+
|
|
980
|
+
> **Aynı kilit LAYOUT'ta da vardır** (ölçüm 2026-08-18: menüye yeni bağlantı eklerken
|
|
981
|
+
> `POST /layouts/<id>/flows` aynı hatayı verdi). Layout'un olayları komponentin kendi `events`
|
|
982
|
+
> dizisinde satır içidir (`{ "name": "click", "value": "<flow id>" }`) ve `page_schema_patch`
|
|
983
|
+
> layout'a uygulanmaz; çözüm de bu yüzden farklı yazılır:
|
|
984
|
+
>
|
|
985
|
+
> ```
|
|
986
|
+
> 1) layout_update → tüm komponentlerde "events": [] (bağları çöz)
|
|
987
|
+
> 2) flow_create × N
|
|
988
|
+
> 3) layout_update → tam ağaç + bütün events'ler
|
|
989
|
+
> ```
|
|
990
|
+
>
|
|
991
|
+
> Sıfırdan kurarken kilide takılmazsın (akışları bağlamadan önce yaratırsın); kilit yalnız
|
|
992
|
+
> **mevcut ve bağlı** bir layout'a yeni akış eklerken çıkar.
|
|
993
|
+
|
|
994
|
+
## Kural 4 — Komponent güncellemesi slug ile, silme id ile
|
|
995
|
+
|
|
996
|
+
`component_update` platformdaki **tek** slug'la çalışan update ucudur
|
|
997
|
+
(`PUT components/{applicationId}/{slug}`); `component_delete` id alır. Widget kaydında
|
|
998
|
+
(`kind:'widget'`) `widgetType` (`container` | `modal` | `drawer`) zorunludur.
|
|
999
|
+
|
|
1000
|
+
`component_list`/`component_get` iki işi görür: (1) sayfaya konabilecek tipleri ve prop
|
|
1001
|
+
sözlüğünü okumak — şemaya **uydurma prop yazma**, (2) kendi yazdığın komponentleri yönetmek.
|
|
1002
|
+
`scope:'all'` platformun varsayılan kataloğunu getirir.
|
|
1003
|
+
|
|
1004
|
+
## Kural 5 — Layout: şema alanı `views` ve MUTLAKA bir `slot` içerir
|
|
1005
|
+
|
|
1006
|
+
`layout_create/update` komponent ağacını `views` alanında taşır (`schema` değil) ve layout'un
|
|
1007
|
+
micro-op hattı **yoktur** — değişiklik tam gövde PUT'tur (gateway merge eder).
|
|
1008
|
+
|
|
1009
|
+
**Slot şart:** sayfa bir layout'a bağlandığında render şeması **layout'un views'ü** olur ve
|
|
1010
|
+
sayfanın kendi içeriği YALNIZ layout'taki `type: 'slot'` komponentinin içine yerleştirilir
|
|
1011
|
+
(Studio `page.mappers.ts`: `mapPageLayoutToPreview` sayfa şemasını slot'un `children`'ına koyar).
|
|
1012
|
+
Slot'suz bir layout'a bağlanan her sayfa — kaydı kusursuz olsa da — **Studio'da ve runtime'da
|
|
1013
|
+
boş görünür**; kayıt tarafında hiçbir hata çıkmaz (kütüphane turu R1). Gateway slot'suz
|
|
1014
|
+
`views`'te LAY-002 uyarısı verir. İskelet:
|
|
1015
|
+
|
|
1016
|
+
```jsonc
|
|
1017
|
+
"views": [
|
|
1018
|
+
{ "elementId": "app_header", "kind": "component", "type": "header", "props": { … },
|
|
1019
|
+
"blocks": [{ "breakpoint": 1024, "size": { "h": 8, "w": 12 }, "position": { "x": 0, "y": 0 } }] },
|
|
1020
|
+
{ "elementId": "content_slot", "kind": "component", "type": "slot", "name": "İçerik",
|
|
1021
|
+
"props": {}, "blocks": [{ "breakpoint": 1024, "size": { "h": 80, "w": 12 }, "position": { "x": 0, "y": 8 } }] }
|
|
1022
|
+
]
|
|
1023
|
+
```
|
|
1024
|
+
|
|
1025
|
+
Doğrulama: `page_state` çıktısındaki `layoutSlots` — **boşsa sayfa içeriği görünmeyecek** demektir.
|
|
1026
|
+
|
|
1027
|
+
> Layout gövdesindeki `parameters` alanını **boş bırak**: domain'de saklanır ama Studio'da
|
|
1028
|
+
> editörü yoktur ve runtime'da okunan bir tüketicisi tespit edilmedi (22 E-8a).
|
|
1029
|
+
|
|
1030
|
+
## Kural 6 — Değişkenler: tek kayıt, `state.page.<key>` ile okunur
|
|
1031
|
+
|
|
1032
|
+
`set_binding` ifadeleri yalnız `page_state`'in döndürdüğü değişken/komponent/microservice
|
|
1033
|
+
isimlerine bağlanır.
|
|
1034
|
+
|
|
1035
|
+
**Platform değişkenleri tek tek değil SET olarak tutar:** uygulama başına bir kayıt, sayfa başına
|
|
1036
|
+
bir kayıt (`variable_create` `pageId` verirsen sayfa, vermezsen uygulama düzeyi). Kayıt zaten varsa
|
|
1037
|
+
create update'e döner. Şekil `[{ key, value }]`; `value` **herhangi bir JSON** olabilir:
|
|
1038
|
+
|
|
1039
|
+
```jsonc
|
|
1040
|
+
variable_create({ applicationId, pageId, variables: [
|
|
1041
|
+
{ "key": "MyTest", "value": { "isActive": true } },
|
|
1042
|
+
{ "key": "sayfaBaslik", "value": "Etkinlikler" }
|
|
1043
|
+
]})
|
|
1044
|
+
```
|
|
1045
|
+
|
|
1046
|
+
- Uç seti **komple değiştirir**; gateway mevcut seti okuyup anahtar bazında birleştirir. Kasten
|
|
1047
|
+
silmek için `replace: true`. `variable_delete` **setin tamamını** siler.
|
|
1048
|
+
- Platform `data`'yı JSON **string** olarak saklar (`"[{\"key\":…}]"`); gateway çeviriyi iki yönde
|
|
1049
|
+
de yapar — `variable_list` sana `[{key, value}]` döndürür. Ham HTTP kullanıyorsan string
|
|
1050
|
+
zorunludur: gövdede yalnız `data` alanı okunur, `name`/`type`/`defaultValue` gibi alanlar
|
|
1051
|
+
**sessizce yok sayılır** ve kayıt boş yazılır (istek yine 200 döner).
|
|
1052
|
+
|
|
1053
|
+
**Okuma yolu — sayfada `state.page.<key>`, uygulama düzeyinde `state.app.<key>`:**
|
|
1054
|
+
|
|
1055
|
+
```jsonc
|
|
1056
|
+
// prop'u doğrudan ifadeye bağlama (flow gerekmez):
|
|
1057
|
+
"text": { "type": "code",
|
|
1058
|
+
"data": "Button text",
|
|
1059
|
+
"ref": "{{ state.page.MyTest.isActive == true ? 'Test' : 'FalseTest' }}" }
|
|
1060
|
+
```
|
|
1061
|
+
|
|
1062
|
+
`type: 'code'` prop'unda **ifade `ref` alanındadır ve `{{ }}` ile sarılır**; `data` tasarım
|
|
1063
|
+
zamanı/yedek değeri taşır. Aynı erişim flow `code` adımlarında ve `value.type:'state'`
|
|
1064
|
+
ref'lerinde de geçerlidir (`platform_guide('expressions')` §2-3). `value` bir nesneyse alanlarına
|
|
1065
|
+
`state.page.<key>.<alan>` ile inilir.
|
|
1066
|
+
|
|
1067
|
+
**Kod prop'ları CANLI bağdır (reaktif).** Runtime, `type:'code'` + `ref` taşıyan HER prop'u
|
|
1068
|
+
otomatik olarak canlı bağa çevirir: ifadenin okuduğu state/komponent değeri her değiştiğinde
|
|
1069
|
+
prop KENDİLİĞİNDEN yeniden hesaplanır — güncellemek için flow kurmak gerekmez (kaynak:
|
|
1070
|
+
zeyrek-frontend `initialization.handler.ts` → `executeStateBindings`, her prop yazımından sonra).
|
|
1071
|
+
Tercih kuralı: *"bu değer başka bir değerden TÜRETİLİYORSA"* `set_binding` / kod prop'u kullan;
|
|
1072
|
+
*"bir OLAY sonucunda bir kez yazılacaksa"* (uçtan veri çekme gibi) flow + setter kullan.
|
|
1073
|
+
İkisini aynı prop'ta karıştırma — setter'ın yazdığı değeri canlı bağ bir sonraki tetikte ezer.
|
|
1074
|
+
|
|
1075
|
+
**`stateBindings[]` (şema düğümündeki dizi)** Studio'nun state-bağımlılık özelliğidir:
|
|
1076
|
+
`[{ name, expression, targets: [{ elementId, elementProperty }] }]` — bir ifade değişince
|
|
1077
|
+
listedeki hedef prop'lara yazılır. Micro-op sözlüğünde karşılığı YOKTUR; yalnız
|
|
1078
|
+
`add_component` gövdesinde ya da `page_update(schema)` ile yazılabilir. Aynı işi kod prop'u
|
|
1079
|
+
zaten gördüğü için yeni kurulumda tercih etme; mevcut sayfada görürsen silme.
|
|
1080
|
+
|
|
1081
|
+
## Kural 7 — Sayfa arketipleri: liste tek başına uygulama değildir
|
|
1082
|
+
|
|
1083
|
+
Bir uygulama, tabloların listelendiği ekranlar toplamı değildir. **CRUD ucu yazdıysan o kaydın
|
|
1084
|
+
yönetim ekranını da kur.** Kullanıcı kavramı varsa giriş ekranı olmadan sistem kullanılamaz.
|
|
1085
|
+
Aşağıdaki dört arketip `qm` projesinin canlı kayıtlarından çıkarıldı (140 sayfa aynı desende);
|
|
1086
|
+
ayrıntılı kanıt ve kayıt id'leri: `zeyrek-mcp/docs/18-sayfa-arketipleri.md`. **Tam gövdeler
|
|
1087
|
+
altın örneklerde** — şemayı ve akışları oradan kopyalayıp uyarla:
|
|
1088
|
+
|
|
1089
|
+
| Arketip | Örnek |
|
|
1090
|
+
|---|---|
|
|
1091
|
+
| Liste | `ngapps://examples/flow-load-table` |
|
|
1092
|
+
| Form (create+edit) | `ngapps://examples/page-form-upsert` |
|
|
1093
|
+
| Liste → form geçişi | `ngapps://examples/flow-list-to-form` |
|
|
1094
|
+
| Giriş | `ngapps://examples/page-sign-in` |
|
|
1095
|
+
| Detay / profil | `ngapps://examples/page-detail-profile` |
|
|
1096
|
+
| Dinamik form (alanlar veriden) | `ngapps://examples/page-dynamic-form` (Kural 1g + 1h) |
|
|
1097
|
+
|
|
1098
|
+
> Gateway iki yarım kurulumu yakalar: **FORM-001** (`form` eklendi, `submit` bağlanmadı) ve
|
|
1099
|
+
> **FORM-002** (akışta POST/PUT adımı var ama hiç `source:'body'` parametresi yok → gövde boş
|
|
1100
|
+
> gider). `project_overview` ayrıca proje düzeyi boşlukları döndürür (`coverageGaps`): kimlik
|
|
1101
|
+
> middleware'i var ama giriş sayfası yok, yazma ucu var ama sayfa yok.
|
|
1102
|
+
|
|
1103
|
+
**Adlandırma:** çoğul = liste sayfası, tekil = form sayfası (`Departmanlar` / `Departman`).
|
|
1104
|
+
Sayfa çiftini bu şekilde kur.
|
|
1105
|
+
|
|
1106
|
+
### 7a — Form sayfası (create + edit AYNI sayfa)
|
|
1107
|
+
|
|
1108
|
+
```
|
|
1109
|
+
page_container
|
|
1110
|
+
├── breadcrumb · notification
|
|
1111
|
+
├── form[ text · form-validation-errors · textinput · textarea · dropdown … ]
|
|
1112
|
+
└── container[ container[ btn_save · btn_cancel ] · container[ audit-basic ] ]
|
|
1113
|
+
```
|
|
1114
|
+
|
|
1115
|
+
`form` komponenti `submit` olayı yayar; `errorElementId` prop'unu sayfadaki
|
|
1116
|
+
`form-validation-errors` komponentine bağla, input'ların `validations`'ı (required/minLength/
|
|
1117
|
+
maxLength/pattern — sözlük `component_get`'te) orada listelenir.
|
|
1118
|
+
|
|
1119
|
+
**Validasyon yazma şekli — DÖRT alan da zorunludur:** `{ type, message, value, isActive }`.
|
|
1120
|
+
|
|
1121
|
+
```jsonc
|
|
1122
|
+
{ "op": "set_validations", "target": "in_email", "validations": [
|
|
1123
|
+
{ "type": "required", "message": "E-posta zorunludur.", "value": true, "isActive": true },
|
|
1124
|
+
{ "type": "email", "message": "Geçerli bir e-posta girin.", "value": true, "isActive": true },
|
|
1125
|
+
{ "type": "max-length", "message": "En fazla 120 karakter.", "value": 120, "isActive": true }
|
|
1126
|
+
] }
|
|
1127
|
+
```
|
|
1128
|
+
|
|
1129
|
+
Geçerli `type` değerleri: `required` · `email` · `regex` · `max-length` · `min-length` ·
|
|
1130
|
+
`max-value` · `min-value` · `value-options`. Üç tuzak ölçüldü (crm koşumu B-9):
|
|
1131
|
+
|
|
1132
|
+
| Yanlış gövde | Ne olur |
|
|
1133
|
+
|---|---|
|
|
1134
|
+
| `{ type, isActive }` (mesaj/değer yok) | Kullanıcı neyin yanlış olduğunu göremez; eksik alanlı gövde sayfayı okunamaz bırakabilir (doküman 23 §B-2). Gateway **PAGE-016** ile reddeder. |
|
|
1135
|
+
| `{ name, value, message }` (`type` yok) | `ValidationModel.Name` **JsonIgnore**'dur: tip enum varsayılanına düşer ve alan SESSİZCE `regex` doğrulamasına döner. |
|
|
1136
|
+
| `{ type: "maxLength" }` (deve harfli) | Backend eski enum üyesiyle kabul edip geri okur ama **runtime doğrulayıcı yalnız tireli adları tanır** (`validation.ts` switch → `default: return true`): doğrulama hiç koşmaz. |
|
|
1137
|
+
|
|
1138
|
+
Gateway ilk iki ve üçüncü durumdan düzeltebildiklerini düzeltir (`name` → `type`, `maxLength` →
|
|
1139
|
+
`max-length`, eksik `isActive` → `true`) ve `gatewayNormalized` ile bildirir; düzeltilemeyen
|
|
1140
|
+
(mesajsız / tanınmayan tip / değersiz eşik) gövdeyi **PAGE-016** ile yazmadan reddeder.
|
|
1141
|
+
Validasyon ya `add_component` gövdesinde baştan verilir ya da sonradan `set_validations` ile
|
|
1142
|
+
değiştirilir.
|
|
1143
|
+
|
|
1144
|
+
Op **listeyi tamamen değiştirir** (boş dizi hepsini siler) ve yalnız o düğümün `validations`
|
|
1145
|
+
alanına dokunur. **Komponenti silip yeniden eklemeyin:** `remove_component` + `add_component`
|
|
1146
|
+
turu o komponente bağlı setter hedeflerini koparır (Kural 1e sonundaki not) ve komponentin tek
|
|
1147
|
+
referans verdiği akışları kalıcı olarak sildirir (Kural 2'deki yan etki notu).
|
|
1148
|
+
|
|
1149
|
+
**Kip ayrımı query parametresidir:** aynı sayfa `?id=<kayıt>` ile edit, parametresiz create.
|
|
1150
|
+
|
|
1151
|
+
`mount` akışı (`page_container` ya da `form` üzerine bağlanır):
|
|
1152
|
+
|
|
1153
|
+
```
|
|
1154
|
+
start → [setter] page_container.isLoading = true
|
|
1155
|
+
→ [condition] Page.queryParams.id !== undefined
|
|
1156
|
+
├─ always → [setter] page_container.isLoading = false
|
|
1157
|
+
├─ if → [setter] EDIT etiketleri (buton metni, breadcrumb.currentTitle, audit görünür)
|
|
1158
|
+
│ → [microservice] get (id query-string'den) → [setter] alanları doldur
|
|
1159
|
+
└─ else → [setter] CREATE etiketleri
|
|
1160
|
+
```
|
|
1161
|
+
|
|
1162
|
+
> **Condition adımı `always` dalını kural dallarıyla BİRLİKTE taşır.** `always` her hâlükârda koşar
|
|
1163
|
+
> (yükleniyor bayrağını kapatmanın yolu budur), kural dalları (`branch.name` = **rule id**) eşleşene
|
|
1164
|
+
> göre. `rules`: `{ id, expression, type:'if' }` + `{ id, expression:'', type:'else' }`.
|
|
1165
|
+
>
|
|
1166
|
+
> **`rules[].id` GERÇEK bir GUID olmalı** — okunabilir bir ad (`"rule_ok"`) verirsen istek
|
|
1167
|
+
> `The JSON value could not be converted to System.Guid. Path: $.flow.steps[N].args.rules[0].id`
|
|
1168
|
+
> ile reddedilir. Dal adı da o GUID'in kendisidir (`branch.name === rule.id`), yani iki yerde de
|
|
1169
|
+
> okunabilir ad kullanamazsın.
|
|
1170
|
+
>
|
|
1171
|
+
> **Kural ifadesi patlarsa akış hata vermez, kuralı EŞLEŞMEMİŞ sayar** ve sessizce `else`'e
|
|
1172
|
+
> gider. Bu yüzden condition ifadeleri guard'lı yazılır — özellikle `Page.user` okuyanlar
|
|
1173
|
+
> (Kural 8).
|
|
1174
|
+
|
|
1175
|
+
**İki ayrı ucun sonucunu tek tabloya yazmak:** `mapping` tek kaynak alır, o yüzden birleştirmeyi
|
|
1176
|
+
`value.type: 'code'` ile yap — ilgisiz kayıtları da orada süz:
|
|
1177
|
+
|
|
1178
|
+
```jsonc
|
|
1179
|
+
{ "type": "change", "element": "tbl", "property": "rows", "source": "component",
|
|
1180
|
+
"mapping": { "source": "", "mappings": {} },
|
|
1181
|
+
"value": { "type": "code", "ref": "[].concat(Steps.load_a?.data || [], Steps.load_b?.data || []).filter(function (r) { return !!r.request_id; }).map(function (r) { return { … }; })" } }
|
|
1182
|
+
```
|
|
1183
|
+
|
|
1184
|
+
> Bir tablo birden çok kayıt tipini besliyorsa (aynı `approval_steps` hem izin hem avans
|
|
1185
|
+
> kademesi tutuyor gibi) **ilgisiz tipi süzmek şart** — süzülmezse karşı tipin satırları
|
|
1186
|
+
> arayüzde **boş satır** olarak belirir ve hiçbir hata çıkmaz.
|
|
1187
|
+
|
|
1188
|
+
Alanları doldurma — kaynak adımın **ADI**, ilişki alanında **`.id`**:
|
|
1189
|
+
|
|
1190
|
+
```jsonc
|
|
1191
|
+
{ "type": "change", "element": "name_f7bc", "property": "value", "source": "component",
|
|
1192
|
+
"mapping": { "source": "", "mappings": {} },
|
|
1193
|
+
"value": { "type": "storage", "ref": "get_service.data.name" } }
|
|
1194
|
+
// ilişki (dropdown) → etiket değil id
|
|
1195
|
+
{ "type": "change", "element": "manager_bb97", "property": "value", "source": "component",
|
|
1196
|
+
"mapping": { "source": "", "mappings": {} },
|
|
1197
|
+
"value": { "type": "storage", "ref": "get_service.data.manager.id" } }
|
|
1198
|
+
// audit kutusu tek prop'ta beslenir
|
|
1199
|
+
{ "type": "change", "element": "auditbasic7f72", "property": "meta", "source": "component",
|
|
1200
|
+
"mapping": { "source": "", "mappings": {} },
|
|
1201
|
+
"value": { "type": "storage", "ref": "get_service.data.meta" } }
|
|
1202
|
+
```
|
|
1203
|
+
|
|
1204
|
+
`submit` akışı:
|
|
1205
|
+
|
|
1206
|
+
```
|
|
1207
|
+
start → [setter] btn_save.isLoading = true
|
|
1208
|
+
→ [microservice] upsert (POST)
|
|
1209
|
+
→ [condition] Steps.upsert_service.error
|
|
1210
|
+
├─ always → [setter] btn_save.isLoading = false
|
|
1211
|
+
├─ if → [setter] notification: kind='error', header, content, isHidden=false
|
|
1212
|
+
└─ else → [condition] Page.queryParams.id
|
|
1213
|
+
├─ if (edit) → [setter] notification: kind='success', timeout=7000
|
|
1214
|
+
└─ else (create) → [redirect] { pageId:<liste>, path:'departmanlar' }
|
|
1215
|
+
```
|
|
1216
|
+
|
|
1217
|
+
**Komponentten uca değer geçirme** — form kablolamasının tamamı bu şekildedir:
|
|
1218
|
+
|
|
1219
|
+
```jsonc
|
|
1220
|
+
"parameters": [
|
|
1221
|
+
{ "name": "id", "type": "number", "source": "query-string",
|
|
1222
|
+
"value": { "type": "code", "ref": "Page.queryParams.id" } },
|
|
1223
|
+
{ "name": "name", "type": "string", "source": "body",
|
|
1224
|
+
"value": { "type": "property",
|
|
1225
|
+
"ref": { "source": "component", "element": "name_f7bc", "property": "value" } } }
|
|
1226
|
+
]
|
|
1227
|
+
```
|
|
1228
|
+
|
|
1229
|
+
- **Upsert deseni:** tek POST ucu hem create hem update yapar — `id` query-string'den, alanlar
|
|
1230
|
+
`source:'body'`'den. Entity başına iki uç yazmaya gerek yok.
|
|
1231
|
+
- **Hata yolunu `errorSteps` ile kurma**, `condition Steps.<adımAdı>.error` ile kur: `errorSteps`
|
|
1232
|
+
ana akışın kalanını hiç çalıştırmaz, yükleniyor bayrağı açık kalır.
|
|
1233
|
+
- Geri bildirim `notification` komponentiyle verilir (`kind` · `header` · `content` · `timeout` ·
|
|
1234
|
+
`showCloseButton` · `style`), `alert` adımıyla değil.
|
|
1235
|
+
|
|
1236
|
+
> **`isHidden`: ŞEMAYA yazmak ile SETTER ile yazmak AYRI yollardır.** Runtime tarafı ortak:
|
|
1237
|
+
> `RenderComponent` her düğümü `{#if !props.isHidden}` ile çizer, yani akıştaki setter
|
|
1238
|
+
> `isHidden` yazınca komponent katalogda o prop'u tanımlamasa bile gizlenir. **Şema yolu ise
|
|
1239
|
+
> katalogla sınırlıdır:** `props.isHidden`'ı düğüme yazmak, komponent onu tanımlamıyorsa
|
|
1240
|
+
> `unknown-prop` ile REDDEDİLİR (81 kaydın 36'sı tanımlar; ölçüm 2026-08-25). Tanımlamayanlar
|
|
1241
|
+
> arasında sık kullanılanlar: **`text` · `table` · `datatable` · `dropdown` · `multi-select` ·
|
|
1242
|
+
> `tag-input` · `tabs` · `checkbox` · `switch` · `switcher` · `card` · `modal` · `drawer` ·
|
|
1243
|
+
> `repeater` · `notification` · `toast` · `datepicker` ailesi · `breadcrumb-title`.**
|
|
1244
|
+
> Tek doğru kaynak `component_get` + `page_schema_validate` kuru koşusudur.
|
|
1245
|
+
>
|
|
1246
|
+
> **`appearance.isHidden` bir kaçış yolu DEĞİLDİR:** kaydedilir ama runtime hiçbir yerde okumaz
|
|
1247
|
+
> (renderer yalnız `props.isHidden`'a bakar) — komponent görünmeye devam eder. Başlangıçta gizli
|
|
1248
|
+
> göstermek için ya komponenti `isHidden`'ı OLAN bir `container`'a sar (Kural 1g) ya da
|
|
1249
|
+
> görünürlüğü sayfa açılış akışının setter'ıyla kur.
|
|
1250
|
+
|
|
1251
|
+
### 7b — Liste → form geçişi
|
|
1252
|
+
|
|
1253
|
+
**Önce hangi tabloyu kullandığını bil — ikisi FARKLI kurulur:**
|
|
1254
|
+
|
|
1255
|
+
| | `table` | `datatable` |
|
|
1256
|
+
|---|---|---|
|
|
1257
|
+
| Yaydığı olaylar | `mount` · `select` · `doubleClick` · `filter` · `sort` · `pagination` … | **yalnız `mount`** |
|
|
1258
|
+
| Süzme/sıralama/sayfalama | her biri AYRI akışa bağlanır | komponent kendi prop'larını (`filters`/`sortField`/`sortType`/`offset`) yazıp `mount`'u yeniden tetikler → **tek akış yeter** |
|
|
1259
|
+
| Satır seçimi / çift tık | var (`self.selectedRow`) | **YOK** — `doubleClick` bağlamak 200 döner ama bağ ölüdür (gateway PAGE-008 keser) |
|
|
1260
|
+
| Kayıt açma | `doubleClick` akışı | satır **aksiyonu**: `actions: [{ text, kind, icon, flow: { type: 'flow', ref: <flowId> } }]` |
|
|
1261
|
+
|
|
1262
|
+
`datatable` satır aksiyonunda tıklanan satır komponentin `selectedRow` prop'una **komponent
|
|
1263
|
+
tarafından** yazılır, sonra akış çalışır (`Table.svelte:72` → `setNode` + `onEmit`): akışta
|
|
1264
|
+
`<elementId>.selectedRow.<alan>` ile okunur. `actions[].kind` sözlüğü **butonunkinden farklıdır**
|
|
1265
|
+
(`error · information · solid · transparent · dark · danger · warning`; butonda
|
|
1266
|
+
`success · error · info · primary · warning`) — yanlış sözlükten değer `invalid-enum-value` verir.
|
|
1267
|
+
|
|
1268
|
+
Aşağıdaki `doubleClick` tarifi **`table`** içindir. Geçişi `redirect` yapar ve
|
|
1269
|
+
**`query` bir İFADEDİR**:
|
|
1270
|
+
|
|
1271
|
+
```jsonc
|
|
1272
|
+
// satıra çift tıkla → edit ; self = tetikleyen komponent
|
|
1273
|
+
{ "stepId": "go_edit", "type": "redirect", "kind": "normal",
|
|
1274
|
+
"args": { "pageId": "<form sayfası id>", "path": "departman",
|
|
1275
|
+
"query": "`id=${self.selectedRow.id}`" },
|
|
1276
|
+
"branches": [{ "id": "<GUID>", "name": "always" }] }
|
|
1277
|
+
|
|
1278
|
+
// "Yeni" butonu → create (bağlamı taşıyarak; komponente ADIYLA erişim)
|
|
1279
|
+
{ "args": { "pageId": "<form sayfası id>", "path": "departman",
|
|
1280
|
+
"query": "table?.selectedRow?.id ? `parent=${table.selectedRow.id}` : ''" } }
|
|
1281
|
+
```
|
|
1282
|
+
|
|
1283
|
+
Sayfalar arası veri geçmenin yolu budur: gönderen `query`'ye yazar, alan `Page.queryParams.<ad>`
|
|
1284
|
+
ile okur. `variable_create`'e ihtiyaç yoktur.
|
|
1285
|
+
|
|
1286
|
+
### 7c — Giriş sayfası (kimlik varsa ZORUNLU)
|
|
1287
|
+
|
|
1288
|
+
```
|
|
1289
|
+
page_container
|
|
1290
|
+
└── auth-layout[ text · notification · form[ textinput · password · checkbox · button ] ]
|
|
1291
|
+
```
|
|
1292
|
+
|
|
1293
|
+
```
|
|
1294
|
+
start → [setter] btn.isLoading = true, btn.isDisabled = true, notification gizle
|
|
1295
|
+
→ [microservice] sign-in (POST; email + password body'den)
|
|
1296
|
+
→ [condition] !!Steps.signIn.data.token
|
|
1297
|
+
├─ always → [setter] buton durumunu sıfırla
|
|
1298
|
+
├─ if → [setcookie] { name:'APP_Token', value:'Steps.signIn.data.token',
|
|
1299
|
+
│ expiresInHours: 240 }
|
|
1300
|
+
│ → [redirect] hedef sayfaya
|
|
1301
|
+
└─ else → [setter] notification.content =
|
|
1302
|
+
{ type:'code', ref:'Steps.signIn.data.message || "Giriş başarısız"' }
|
|
1303
|
+
```
|
|
1304
|
+
|
|
1305
|
+
- **Cookie adı middleware'in okuduğu adla birebir aynı olmalı** (`cookie.APP_Token` —
|
|
1306
|
+
`platform_guide('middleware')` Tarif 1). Uyuşmazlık sessizce "hep 401" demektir.
|
|
1307
|
+
- `redirect` adımı `isRaw: true` iken `path` **çok satırlı JS bloğudur** (son ifade değeri döner) —
|
|
1308
|
+
"korunan sayfaya gitmek isterken login'e düşen kullanıcıyı geri gönderme" böyle kurulur.
|
|
1309
|
+
- Prop değeri nesne olabilir: `icon` → `{ "type": "static", "ref": { "name": "progress_activity" } }`.
|
|
1310
|
+
- Giriş sayfası kimlik işinin görünen yarısıdır. Sign-in ucu + auth middleware'i kurup bu sayfayı
|
|
1311
|
+
kurmamak, sistemi kullanılamaz bırakır.
|
|
1312
|
+
|
|
1313
|
+
### 7d — Detay / profil sayfası
|
|
1314
|
+
|
|
1315
|
+
Form arketipinin salt-okuma akrabası: input'lar `isViewmode: true`, altında ilişkili kayıt tablosu,
|
|
1316
|
+
`modal`/`drawer` ile ikincil işlemler (parola güncelleme gibi — modalın içinde kendi formu ve kendi
|
|
1317
|
+
`submit` akışı). Kimliği query parametresinden değil **middleware'den** alır: uca auth middleware'i
|
|
1318
|
+
bağlanır, filtre `source:'expression'` + `value:'user.id'` olur.
|
|
1319
|
+
|
|
1320
|
+
### 7e — Hangi iş için hangi komponent
|
|
1321
|
+
|
|
1322
|
+
`component_get` prop sözlüğünü verir ama **hangi işe hangisi** kararını sen verirsin:
|
|
1323
|
+
|
|
1324
|
+
| İş | Komponent |
|
|
1325
|
+
|---|---|
|
|
1326
|
+
| Metin / uzun metin / zengin metin | `textinput` · `textarea` · `texteditor` |
|
|
1327
|
+
| Öneri listeli serbest metin | `autocomplete` (value YAZILAN metindir; id gerekiyorsa `select` olayı) |
|
|
1328
|
+
| Sayı / artan kod / kaydırıcı | `numberinput` · `incrementalinput` · `slider` |
|
|
1329
|
+
| Seçim (tek / çok / etiket) | `dropdown` · `multi-select` · `tag-input` · `radio` · `checkboxgroup` (değeri dizi) |
|
|
1330
|
+
| İki durumlu | `checkbox` · `switch` · `switcher` (iki durum yan yana) |
|
|
1331
|
+
| Puan / kod girişi | `rating` · `otpinput` |
|
|
1332
|
+
| Tarih / saat / aralık | `datepicker` · `datetimepicker` · `daterangepicker` · `datetimerangepicker` · `timepicker` (yalnız HH:mm) |
|
|
1333
|
+
| Kimlik alanları | `email` · `password` |
|
|
1334
|
+
| Dosya | `fileUpload` (dropzone) · `fileInput` · `filebutton` · `file-viewer` |
|
|
1335
|
+
| Liste | `table` · `datatable` (Kural 1e) · `repeater` (kart listesi VE dinamik form — Kural 1g) · `tree-view` (hiyerarşi; `isSearchable` ile istemci içi arama) · `timeline` (tarihli olay akışı) |
|
|
1336
|
+
| Sayfalama (tablo dışı listeler için) | `pagination` — offset/limit yazar, `self.offset`/`self.limit` ile sorguya bağlanır |
|
|
1337
|
+
| Grafik / KPI | `chart` (ECharts: bar·line·area·pie·donut + waterfall/histogram/pareto/bullet/tornado/radial) · `statcard` (tek KPI + trend + sparkline) · `progress` |
|
|
1338
|
+
| Kapsayıcı / düzen | `container` (ızgara ya da flex — Kural 1f-2) · `form` (düzen prop'u YOK, çocuklar ızgara koordinatıyla dizilir) · `tabs` · `collapsible-container` · `accordion` |
|
|
1339
|
+
| Kart görünümü | `card` — **kapsayıcı DEĞİLDİR**, çocuk kabul etmez (`not-droppable`) ve yüksekliği 10-50 aralığındadır. Kutu içinde serbest içerik için `container` kullan. |
|
|
1340
|
+
| Katman | `modal` · `drawer` (şema kökünde — Kural 1d) |
|
|
1341
|
+
| Geri bildirim | `notification` (sayfa içi kutu) · `toast` (geçici bildirim; isOpen yükselen kenarı) · `form-validation-errors` · `confirm-bar` · `result` (işlem sonucu ekranı) · `emptystate` (boş liste; ikon YA DA görsel + aksiyon düğmeleri) |
|
|
1342
|
+
| Gezinme / başlık | `breadcrumb` · `breadcrumb-title` (başlık + daralan iz) · `page-header` · `header` · `sidebar` · `auth-layout` · `language-selector` (dil değiştirici) |
|
|
1343
|
+
| Sunum | `text` · `html` · `image` · `video` · `icon` · `qrcode` · `tags` · `avatar` · `badge` (sayaç “99+” / nokta / durum metni) · `carousel` · `iframe` (sandbox’lı gömme) · `jsonviewer` (katlanabilir JSON; JSON METNİNİ de parse eder) · `audit-basic` · `audit-history` |
|
|
1344
|
+
|
|
1345
|
+
> **`chart` komponenti VAR (2026-08-25 ölçümü) — bu satır düzeltildi.** Rehberin önceki hâli
|
|
1346
|
+
> "grafik komponenti yoktur, `html` + `loadScript` kullanın" diyordu; `component_list` çıktısında
|
|
1347
|
+
> `chart` kayıtlıdır ve `component_get(slug:'chart')` prop sözlüğünü verir. Veri `rows` prop'una
|
|
1348
|
+
> akışla yazılır (komponent kendi verisini ÇEKMEZ), kategori `categoryField`, değerler
|
|
1349
|
+
> `series[].valueField` ile çözülür; `aggregate`/`sort`/`topN` dönüşümü komponent yapar.
|
|
1350
|
+
> `html` + `loadScript` yalnız `chart`'ın kapsamadığı görselleştirmeler için gerekir.
|
|
1351
|
+
>
|
|
1352
|
+
> **Veriye göre değişen form (çalışma anında N alan) `html` GEREKTİRMEZ — `repeater` bunu
|
|
1353
|
+
> yapar.** Girdiler şemaya tasarım anında yazılır ama `repeater` o şablonu `rows`'un her satırı
|
|
1354
|
+
> için yineler, `{{ item.<alan> }}` kod prop'u iki yönlü bağlanır ve alan tipi satır başına
|
|
1355
|
+
> `isHidden` ile seçilir. Anket / dinamik başvuru formu bu desenle kurulur — tam tarif ve
|
|
1356
|
+
> ölçülmüş sınırlar **Kural 1g**'de. (Bu satır 2026-08-19'da düzeltildi: önceki hâli "hazır
|
|
1357
|
+
> komponent yoktur, `html` kullanın" diyordu ve YANLIŞTI.)
|
|
1358
|
+
|
|
1359
|
+
**Hiçbiri işi karşılamıyorsa `html` kaçış kapısıdır** — Mustache + `context` ile veri basar,
|
|
1360
|
+
`actions` + `fireEvent` ile flow tetikler, satır içi script çalıştırır. "Platform bunu yapamaz"
|
|
1361
|
+
demeden önce `platform_guide('html')` oku; genel komponent modeli için `platform_guide('component')`.
|
|
1362
|
+
|
|
1363
|
+
Komponentlerin **`style` prop'u** vardır ve iç parçalara iner (`textinput.style`:
|
|
1364
|
+
`&__label` · `&__input` · `&__error` · `&__description`; `form.style`: `&__title` · `&__header` ·
|
|
1365
|
+
`&__body`). Tasarım kararlarını buradan uygula — varsayılan görünümle bırakmak zorunda değilsin.
|
|
1366
|
+
|
|
1367
|
+
> **Boyut tuzağı:** katalog varsayılan `size`'ı `maxSize`'ı aşan komponentler var
|
|
1368
|
+
> (`textinput` default `h:8`, `maxSize.h:5`). Varsayılanı körü körüne kopyalarsan doğrulayıcı
|
|
1369
|
+
> reddeder; gateway aşan değeri `maxSize`'a kırpar ve `gatewayNormalized` ile bildirir.
|
|
1370
|
+
|
|
1371
|
+
## Kural 8 — Korunan sayfa: `middlewareIds` vermeden `Page.user` YOKTUR
|
|
1372
|
+
|
|
1373
|
+
Uçları middleware'e bağlamak sayfayı korumaz ve **`Page.user`'ı doldurmaz**. `Page.user`,
|
|
1374
|
+
sayfaya `page_update` ile bağlanan middleware'in `set-user` çıktısıdır; bağ yoksa `undefined`
|
|
1375
|
+
gelir ve `Page.user.x` okuyan her ifade *"Cannot read properties of undefined"* ile patlar.
|
|
1376
|
+
Bir `condition` adımında bu **sessizce else dalına düşmek** demektir.
|
|
1377
|
+
|
|
1378
|
+
```
|
|
1379
|
+
page_update({ pageId, middlewareIds: ['<authenticate workflow id>'] })
|
|
1380
|
+
```
|
|
1381
|
+
|
|
1382
|
+
Ölçüm (klinik/kutuphane/obs, 2026-08-18): 42 ucun 41'i middleware'e bağlıyken **sayfaların
|
|
1383
|
+
hiçbiri** bağlı değildi; üç portalın da rol menüsü boş çıkıyor, panel ifadeleri patlıyordu.
|
|
1384
|
+
|
|
1385
|
+
İki şeyi karıştırma:
|
|
1386
|
+
|
|
1387
|
+
| İstenen | Yol |
|
|
1388
|
+
|---|---|
|
|
1389
|
+
| Veri güvenliği | **ucun** `middlewareIds` + filtre `source:'expression'`, `value:'user.id'` |
|
|
1390
|
+
| `Page.user` + sayfa yönlendirmesi | **sayfanın** `middlewareIds`'i |
|
|
1391
|
+
|
|
1392
|
+
> **`page.roleIds` yetki KAPATMAZ — güvenlik olarak ona güvenme.** Ölçüm (2026-08-18): `roleIds`
|
|
1393
|
+
> yalnız yönetici rollerine ayarlanmış bir sayfayı hasta rolündeki kullanıcı **açabildi** (sayfa
|
|
1394
|
+
> çizildi, tablo boş kaldı çünkü **uçlar** 401/403 verdi). `roleIds` menü/görünürlük kararı için
|
|
1395
|
+
> bir sinyaldir; erişimi kapatan tek şey **ucun** rol kapısıdır. **Ama `microservice.roles` de
|
|
1396
|
+
> tek başına kapı değildir** (ölçüm 2026-08-19: `roleIds` dolu + `middlewareIds` boş uç,
|
|
1397
|
+
> tokensız POST'u 200 ile kabul etti). `roles` yalnız middleware'e `triggerSource.roles` olarak
|
|
1398
|
+
> taşınan veridir; kapıyı kuran şey middleware içindeki açık `Authorized(user.roles)` adımıdır
|
|
1399
|
+
> (`platform_guide('middleware')` Tarif 3b). Bir ekranı gerçekten kapatmak istiyorsan verisini
|
|
1400
|
+
> besleyen her ucu `middlewareIds` + `Authorized` kapısına al; sayfayı gizlemek kozmetiktir.
|
|
1401
|
+
>
|
|
1402
|
+
> **Sayfa kapısı VARDIR ama onu `redirect` DEĞİL, `redirect`'i izleyen `response` tutar.**
|
|
1403
|
+
> *(Ölçüm 2026-08-19 — bu bölümün önceki sürümü yanlıştı.)* `RedirectStep.cs` yalnız bir
|
|
1404
|
+
> `UiAction` kaydedip success dalından devam eder; durum kodunu değiştirmez. Akış `response`
|
|
1405
|
+
> adımı olmadan biterse sonuç 2xx kalır, `ExecutePageBehavior` kesmez ve **sayfanın tamamı
|
|
1406
|
+
> anonim isteğe döner** — üstelik `PagesController.HandleResult` `uiActions`'ı yalnız non-2xx
|
|
1407
|
+
> dalında yayınladığı için yönlendirme de kaybolur. Ölçüm: yalnız `redirect` ile biten guard'a
|
|
1408
|
+
> bağlı sayfa tokensız istekte **HTTP 200 + şemanın tamamı** döndürdü.
|
|
1409
|
+
>
|
|
1410
|
+
> Doğru desen (ölçülerek doğrulandı — 401, şema sızmadı, `uiActions` yönlendirmeyi taşıdı):
|
|
1411
|
+
>
|
|
1412
|
+
> ```
|
|
1413
|
+
> guard → [condition] triggerSource.type === 'page'
|
|
1414
|
+
> ├─ if → [redirect] expression: "'giris'" → [response] 401 ← kapı BURADA
|
|
1415
|
+
> └─ else → [response] 401 ← uç çağrısı 401 alır
|
|
1416
|
+
> ```
|
|
1417
|
+
>
|
|
1418
|
+
> `redirect` adımının `nextStepId`'si `null` değil, **`response` adımını** göstermeli. Tam gövde
|
|
1419
|
+
> ve tırnak tuzağı (`"'giris'"` — tırnaksız yazarsan runtime'da `giris is not defined`):
|
|
1420
|
+
> `platform_guide('middleware')` Tarif 1b. **Korumalı her sayfa, tokensız bir istekle 401
|
|
1421
|
+
> verdiği görülerek kabul edilir;** 200 + şema dönüyorsa kapı kurulmamıştır.
|
|
1422
|
+
>
|
|
1423
|
+
> **Tokensız istek nasıl atılır:** `page_runtime_get` + **`anonymous: true`** — gateway o çağrıya
|
|
1424
|
+
> hiçbir `Authorization` header'ı koymaz; korumalı sayfada `middlewareBlocked` (401) dönmeli ve
|
|
1425
|
+
> şema gelmemelidir. Sayfanın ham serve yolu **`GET /{project}/{application}/p/{sayfa slug'ı}`**
|
|
1426
|
+
> (`PagesController:127` — `{project}` öneki ZORUNLUDUR; `/{app}/p/{slug}` tek başına 404 verir),
|
|
1427
|
+
> `Host: <org>.<suffix>` + `X-Environment` başlıklarıyla. Ham `curl` yerine tool'u kullanmak
|
|
1428
|
+
> yeterlidir.
|
|
1429
|
+
>
|
|
1430
|
+
> `page.roleIds` bu dalla birlikte de kapı değildir: rol ayrımını sayfa değil **uç** yapar.
|
|
1431
|
+
|
|
1432
|
+
> **Rol okurken dizi bekleme.** `set-user` DTO'su `roles` dizisini çoğu kurulumda düşürür
|
|
1433
|
+
> (`dto_get` ile bak: genelde `role`, `role_name`, `roles_csv` vardır). Sayfa tarafında CSV
|
|
1434
|
+
> kullan — ayrıntı ve şablon `platform_guide('expressions')` §2.
|
|
1435
|
+
>
|
|
1436
|
+
> **Uç hata dönerse `.data` dizi değildir.** 401/404 sonrası `Steps.x.data` bir nesnedir ve
|
|
1437
|
+
> `rows || []` bunu yakalamaz (nesne truthy) → `rows.filter is not a function` /
|
|
1438
|
+
> `(items ?? []).map is not a function`. `Array.isArray(Steps.x?.data) ? Steps.x.data : []` yaz.
|
|
1439
|
+
|
|
1440
|
+
## ⚠️ `page_schema_patch` `html` komponentini reddediyor — katalogda mükerrer kayıt (2026-08-19)
|
|
1441
|
+
|
|
1442
|
+
`page_schema_patch` ile `html` eklemeye çalışırsan:
|
|
1443
|
+
|
|
1444
|
+
```
|
|
1445
|
+
Component type 'html' has no prop named 'context' → kabul edilenler: content
|
|
1446
|
+
Component type 'html' has no prop named 'style'
|
|
1447
|
+
Height of 'html' must be between 1 and 1
|
|
1448
|
+
```
|
|
1449
|
+
|
|
1450
|
+
Oysa `component_get('html')` `content · context · style · actions · isHidden` prop'larını ve
|
|
1451
|
+
`maxSize {w:12,h:50}` döndürür. Çelişkinin sebebi **katalogdaki mükerrer satırlardır**:
|
|
1452
|
+
`development.components` tablosunda 11 slug'ın ikişer kaydı var —
|
|
1453
|
+
`button · checkbox · container · html · image · notification · numberinput · password · text ·
|
|
1454
|
+
textarea · textinput`. `ComponentRepository.FindBySlugsWithDefinitionsAsync` yalnız slug'a göre
|
|
1455
|
+
süzer (tekillik filtresi yok), `PageSchemaValidator` da `GroupBy(Slug).First()` yapar: **ilk
|
|
1456
|
+
satır kazanır ve sıralama garanti değildir.** `html`'de eski/fakir kayıt (tek prop `content`,
|
|
1457
|
+
0 event) gerçek kaydı (8 prop, `mount` olayı) gölgeliyor.
|
|
1458
|
+
|
|
1459
|
+
Diğer 10 slug bugün çalışıyor çünkü zengin kayıt daha eski — bu tasarım değil sıralama şansıdır.
|
|
1460
|
+
|
|
1461
|
+
**Neden yalnız micro-op hattı etkileniyor:** prop/boyut denetimi yalnız
|
|
1462
|
+
`PageSchemaValidationMode.Full`'de yapılır. `page_create`/`page_update` `Structural` çağırır,
|
|
1463
|
+
`layout_*` doğrulayıcıya hiç uğramaz. `Full` kullanan tek iki yer `page_schema_patch` ve
|
|
1464
|
+
`page_schema_validate`'tir.
|
|
1465
|
+
|
|
1466
|
+
**Geçici çözüm (doğrulandı):** `html` içeren sayfayı `page_update(schema)` ile **tam gövde**
|
|
1467
|
+
yaz — `content`/`context`/`style`/`actions` prop'ları ve `events: [{ name:'mount', value:<flowId> }]`
|
|
1468
|
+
bağı kalıcı olur ve `page_runtime_get` paketinde görünür. Bedeli: o sayfada doğrulama duvarı,
|
|
1469
|
+
canlı canvas güncellemesi ve `baseVersion` iyimser kilidi devre dışı kalır. Kusur düzeltilince
|
|
1470
|
+
micro-op hattına dönün.
|
|
1471
|
+
|
|
1472
|
+
> **Sıra kritik — `page_update(schema)` mevcut flow'ların setter hedeflerini KOPARIR.**
|
|
1473
|
+
> Setter iteratee'sindeki `element` bir string olarak saklanmaz: platform onu **kayıt anında**
|
|
1474
|
+
> sayfanın komponentlerine karşı çözüp `DesignComponentId`'ye çevirir. `page_update(schema)` ise
|
|
1475
|
+
> komponent satırlarını yeniden üretir (yeni id'ler) — daha önce kurulmuş her flow'un setter
|
|
1476
|
+
> hedefi böylece dangling kalır ve `flow_get` çıktısında **`element` alanı kaybolur**. Semptom:
|
|
1477
|
+
> akış koşar, microservice 200 döner, ama komponent hiç güncellenmez (ölçüm: anket projesi,
|
|
1478
|
+
> 2026-08-19 — `html` sonsuza kadar "yükleniyor" durumunda kaldı).
|
|
1479
|
+
>
|
|
1480
|
+
> Doğru sıra: komponentleri yaz → `page_update(schema)` ile `actions`/`events` bağlarını kur →
|
|
1481
|
+
> **en son** flow'ları oluştur. Şemayı sonradan yeniden yazmak zorunda kaldıysan **etkilenen her
|
|
1482
|
+
> flow'u `flow_update` ile yeniden kaydet** (aynı gövde yeter; kayıt sırasında hedefler tekrar
|
|
1483
|
+
> çözülür) ve `flow_get` ile `element` alanının geri geldiğini doğrula.
|
|
1484
|
+
>
|
|
1485
|
+
> **Aynı tuzak micro-op yolunda da var:** `page_schema_patch` ile bir düğümü
|
|
1486
|
+
> `remove_component` + `add_component` yapıp **aynı `elementId`'yi** geri koyduğunda satırın
|
|
1487
|
+
> `DesignComponentId`'si yenidir — o düğümü hedefleyen setter'lar yine dangling kalır. Ölçüm
|
|
1488
|
+
> (anket-native, 2026-08-19): `q_list` repeater'ı `isWrapper`'ı düzeltmek için yeniden kurulunca
|
|
1489
|
+
> `t_title`/`t_desc` setter'ları çalışmaya devam etti (dokunulmamışlardı) ama `q_list.rows`
|
|
1490
|
+
> setter'ı sessizce hiçbir şey yazmadı — sayfa boş liste ile açıldı, konsolda hata yok.
|
|
1491
|
+
> `flow_update` ile aynı gövdeyi yeniden kaydetmek düzeltti. Kural: **şemadan bir düğüm sildiysen,
|
|
1492
|
+
> o düğümü hedefleyen her flow'u yeniden kaydet.**
|
|
1493
|
+
|
|
1494
|
+
## Bilinen sınırlar
|
|
1495
|
+
|
|
1496
|
+
- **Ekranı tarayıcıdan doğrularken: `dropdown` ve `multi-select` seçeneklerine tıklamak için iç
|
|
1497
|
+
`<button>`'ı hedefle.** Bu iki komponentin açılır listesinde satır sarmalayıcıları (`div`) tıklama
|
|
1498
|
+
dinlemiyor; asıl dinleyici `button.ng-select-item__content` (dropdown) ve
|
|
1499
|
+
`button.ng-multiselect-item__content` (multi-select). Ölçüm (2026-08-19): satır `div`'ine tam
|
|
1500
|
+
pointer dizisi (`pointerdown/mousedown/pointerup/mouseup/click`) göndermek değeri DEĞİŞTİRMEDİ
|
|
1501
|
+
(`in_status.value` `draft` kaldı); iç `button`'a düz `.click()` göndermek değiştirdi
|
|
1502
|
+
(`published`). Listeyi açmak için ise `.ng-select__value-container` / `.ng-multiselect`'e tam
|
|
1503
|
+
pointer dizisi gerekir — düz `.click()` açmıyor. Değeri doğrulamanın kesin yolu DOM değil
|
|
1504
|
+
`machine.getNode('<elementId>').props.value`'dur.
|
|
1505
|
+
- **`style` prop'una CSS STRING yazma — nesne olmak zorunda.** String de kabul edilir ve emotion onu
|
|
1506
|
+
CSS metni sayar, ama (a) `--ng-*` token ezemezsin, (b) iç seçici / pseudo-class yazamazsın,
|
|
1507
|
+
(c) yanlışlıkla kabın kendi düzenini bozarsın. Ölçüm (anket-admin giriş sayfası, 2026-08-19):
|
|
1508
|
+
`page_container.style` string olarak `display:flex; align-items:center; justify-content:center`
|
|
1509
|
+
taşıyordu; `.ng-container` zaten `display:flex; flex-direction:column` olduğu için kabın iç
|
|
1510
|
+
düzeni çöktü — kart **66px genişlik × 733px yükseklik**, buton 44px çizildi. Aynı ekran nesne
|
|
1511
|
+
style + Kural 1h deseniyle yeniden kurulunca kart 400px'e ve ortaya oturdu. Gateway bunu **PAGE-014 (uyarı)** ile
|
|
1512
|
+
yanıtın `layoutWarnings` alanında bildirir. **Düzeni `style` ile kurmaya hiç kalkışma:**
|
|
1513
|
+
ortalama/yaslama/aralık işi kapsayıcının kendi `layout: 'flex'` prop grubuna aittir (Kural 1f-2).
|
|
1514
|
+
- **`isHidden` olmayan komponent listesine `dropdown` da dahildir** (`text`, `multi-select`,
|
|
1515
|
+
`dropdown`). Koşullu göstermek için `isHidden`'ı olan bir `container`'a sar (Kural 1g).
|
|
1516
|
+
- **Ana sayfayı TAŞIYAMAZSIN — `application_update.homepageId` kalıcı DEĞİL ve yanıtı yanıltıcı.**
|
|
1517
|
+
`ApplicationService.UpdateAsync` sayfaları `AsNoTrackingWithIdentityResolution()` ile okunmuş
|
|
1518
|
+
**takipsiz** bir graf üzerinde `SetHomepage(...)` ile işaretliyor; yanıttaki `homepage` alanı bu
|
|
1519
|
+
bellek içi değişiklikten üretildiği için **yeni sayfayı gösterir ama `pages.is_homepage` diskte
|
|
1520
|
+
değişmez** (ölçüm 2026-08-19: çağrıdan sonra `page_get` eski sayfada hâlâ `isHomepage: true`,
|
|
1521
|
+
yeni sayfada `false`). Sonucu: **ana sayfa silinemez** (`CantDeleteHomepage`) ve gateway'de bunu
|
|
1522
|
+
düzeltmenin yolu yoktur — `UpdatePageCommand`'da `IsHomepage` alanı yok.
|
|
1523
|
+
Daha kötüsü **iki ayrı ana sayfa kapısı var ve birbirine düşebilir:** çağrıdan sonra eski sayfa
|
|
1524
|
+
`pages.is_homepage` yüzünden ("… ana sayfadır ve silinemez"), YENİ sayfa da application'ın
|
|
1525
|
+
referansı yüzünden ("The home page can't be deleted") silinemez hâle geldi; kilidi açmak için
|
|
1526
|
+
`application_update`'i eski sayfaya geri çevirmek gerekti.
|
|
1527
|
+
**Pratik kural:** ana sayfayı değiştirmen gerekiyorsa sayfayı silip yeniden kurma —
|
|
1528
|
+
**mevcut ana sayfanın İÇİNİ** değiştir (`remove_component` + `add_component`), böylece slug,
|
|
1529
|
+
ana sayfa bayrağı ve gelen bağlantılar korunur. Gerçek ana sayfa taşıma Studio'dan yapılmalı.
|
|
1530
|
+
- **Akış adımlarının `branches[].id` ve `iteratees[].id` GUID'leri TÜM projede tekildir.** Başka
|
|
1531
|
+
bir sayfadaki akıştan kopyaladığın gövdeyi aynı GUID'lerle `flow_create`'e verirsen platform
|
|
1532
|
+
`RowAlreadyExists` döner — hata mesajı slug çakışması gibi okunur ve seni yanlış yere götürür
|
|
1533
|
+
(ölçüm 2026-08-19: slug'ı değiştirmek çözmedi, GUID'leri değiştirmek çözdü). Şablon olarak
|
|
1534
|
+
`flow_get` çıktısı kullanırken **bütün id alanlarını yenile**.
|
|
1535
|
+
- **Katalog kaydı frontend'in gerisinde kalabiliyor — doğrulama duvarı frontend'i değil KATALOĞU
|
|
1536
|
+
bilir.** Bir prop'un renderer'da çalışıyor olması onu yazabileceğin anlamına gelmez; **tek
|
|
1537
|
+
referans `component_get`'tir** (platform o satırı `unknown-prop` duvarında kullanır). Yaşanmış
|
|
1538
|
+
örnek ve **kapanışı**: `container`'ın flex düzen grubu (`layout`, `direction`,
|
|
1539
|
+
`horizontalAlign`, `verticalAlign`, `size`, `gap`, `wrap`) 2026-08-19 ölçümünde `ng-apps-ui`
|
|
1540
|
+
şemasında ve renderer'da vardı ama katalog satırında YOKTU — `page_schema_patch` hepsini
|
|
1541
|
+
`unknown-prop` ile reddediyordu. **2026-08-25 ölçümü: katalog güncellenmiş**, yedi prop da
|
|
1542
|
+
`component_get(slug:'container')` çıktısında (varsayılanlarıyla birlikte) ve
|
|
1543
|
+
`page_schema_validate` hepsine `valid: true` diyor — flex kapsayıcı artık şemadan kurulur
|
|
1544
|
+
(Kural 1f-2). Aynı sınıf sorunun HÂLÂ açık örneği: `text` ve `multi-select` komponentlerinde
|
|
1545
|
+
`isHidden` prop'u yok (Kural 1g'deki sarmalayıcı çözümü). Bir prop'u kullanmadan önce
|
|
1546
|
+
`component_get`'e bak; eksikse ürün sahibine katalog güncellemesi olarak bildir.
|
|
1547
|
+
|
|
1548
|
+
- **`container.title` `states.isWrapper: true` iken başlık ÇİZMEZ.** Wrapper kaplar salt düzen
|
|
1549
|
+
elemanıdır; başlığı da `__header` bloğunu da render etmez (klinik ölçümü: `__header` sayısı 0).
|
|
1550
|
+
`isWrapper: false` olan kapta başlık normal çizilir (İK ölçümü: başlık verilmeyen kapta
|
|
1551
|
+
varsayılan "Container title" göründü). Kart başlığını garantiye almak istiyorsan kabın
|
|
1552
|
+
`title`'ına güvenme, **içine bir `text` komponenti** koy.
|
|
1553
|
+
- **Kod prop'unun ifadesi `ref`'te ve `{{ }}` sarılı olmalı.** `{type:'code', ref:"'' + Row.value"}`
|
|
1554
|
+
(süssüz) ekrana ifadenin **kendisini** basar; ifade yalnız `data`'da olursa prop **boş**
|
|
1555
|
+
render edilir. `set_binding` micro-op'u eski sürümlerde `data`'ya yazıyordu — bu davranış
|
|
1556
|
+
düzeltildi, ama daha önce kurulmuş sayfalarda kalıntı olabilir. Prop nesnesini birebir
|
|
1557
|
+
kontrol etmen gerekirse `remove_component` + `add_component` ile
|
|
1558
|
+
`{"type":"code","ref":"{{ … }}","data":"<yedek>"}` yaz; `replace_prop` her zaman `static` kurar.
|
|
1559
|
+
|
|
1560
|
+
- **Ana sayfa sayfadan değil application'dan değişir:** `page_create/update` gövdesindeki
|
|
1561
|
+
`isHomepage` platformca yok sayılır (tasarım). Doğru yol `application_update` →
|
|
1562
|
+
`homepageId`; platform o çağrıda uygulamanın tek sayfasını ana sayfa yapar. `page_delete`
|
|
1563
|
+
ana sayfayı reddeder — gateway bunu istek göndermeden söyler.
|
|
1564
|
+
- **Runtime çıktısı `page_runtime_get` ile okunur:** Studio preview'u ve çıkan uygulama aynı
|
|
1565
|
+
ucu (`GET {application}/p/{slug}`) tüketir; bu tool birleşik son hâli (şema + layout +
|
|
1566
|
+
olaylar + flow'lar) döner ve sayfaya bağlı middleware'ler çağrıda GERÇEKTEN koşar —
|
|
1567
|
+
korumalı sayfada `middlewareBlocked` dönmesi beklenen davranıştır ve kendisi bir
|
|
1568
|
+
doğrulamadır. Tam doğrulama seti: `page_schema_validate` (kuru koşu) + `page_get` (bağlar)
|
|
1569
|
+
+ `page_runtime_get` (son hâl).
|
|
1570
|
+
- **Sayfada dosya toplanabilir:** `fileUpload` (dropzone) · `fileInput` · `filebutton`
|
|
1571
|
+
komponentleri vardır, göstermek için `image` · `file-viewer`. Yani "dosya yükleyerek cevapla"
|
|
1572
|
+
tipi bir alan kurulabilir. Gateway tarafında `file_upload`/`file_download` tool'ları eklendi
|
|
1573
|
+
(multipart / `outPath`) ama **canlı platforma karşı henüz doğrulanmadı** — ilk kullanımda
|
|
1574
|
+
sonucu teyit et.
|