@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,1309 @@
|
|
|
1
|
+
import { logStep } from '../observability/trace.js';
|
|
2
|
+
const SORT_DEFAULT = { name: 'sortField', type: 'sortType', source: 'query-string' };
|
|
3
|
+
const LIMIT_DEFAULT = { name: 'limit', offset: 'offset', source: 'query-string' };
|
|
4
|
+
function asArray(value) {
|
|
5
|
+
return Array.isArray(value) ? value : [];
|
|
6
|
+
}
|
|
7
|
+
function asObjects(value) {
|
|
8
|
+
return asArray(value).filter((entry) => Boolean(entry) && typeof entry === 'object');
|
|
9
|
+
}
|
|
10
|
+
function fillJoinPaths(rootEntityId, parentJoins, chain, aliasChains, fixes) {
|
|
11
|
+
for (const join of asObjects(parentJoins)) {
|
|
12
|
+
const alias = String(join.alias ?? '');
|
|
13
|
+
const relationFieldId = String(join.fieldId ?? '');
|
|
14
|
+
if (!relationFieldId)
|
|
15
|
+
continue;
|
|
16
|
+
const nextChain = [...chain, relationFieldId];
|
|
17
|
+
if (!join.path) {
|
|
18
|
+
join.path = [rootEntityId, ...nextChain].join('.');
|
|
19
|
+
fixes.push(`joins['${alias}'].path dolduruldu`);
|
|
20
|
+
}
|
|
21
|
+
if (alias)
|
|
22
|
+
aliasChains.set(alias, nextChain.join('.'));
|
|
23
|
+
fillJoinPaths(rootEntityId, join.joins, nextChain, aliasChains, fixes);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function fillSelectionPaths(parameters, aliasChains, fixes) {
|
|
27
|
+
for (const key of ['selectFields', 'returnFields', 'filters']) {
|
|
28
|
+
for (const entry of asObjects(parameters[key])) {
|
|
29
|
+
const expected = aliasChains.get(String(entry.alias ?? ''));
|
|
30
|
+
if (expected === undefined || entry.path)
|
|
31
|
+
continue;
|
|
32
|
+
entry.path = expected;
|
|
33
|
+
fixes.push(`${key}['${entry.alias}'.${entry.fieldSlug ?? entry.fieldId}].path dolduruldu`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function collectJoinAliases(joins, into) {
|
|
38
|
+
for (const join of asObjects(joins)) {
|
|
39
|
+
const alias = String(join.alias ?? '');
|
|
40
|
+
if (alias)
|
|
41
|
+
into.add(alias);
|
|
42
|
+
collectJoinAliases(join.joins, into);
|
|
43
|
+
}
|
|
44
|
+
return into;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Kök tablo alias'ı JOIN'in SOL tarafını üretir: `PostgreSqlRelationHandler.HandleJoin`
|
|
48
|
+
* `ProcessJoinTree(entity, alias, joins)` çağrısında gelen alias'ı doğrudan
|
|
49
|
+
* `$"{currentAlias}.{relationField.Slug}"` olarak SqlKata'ya verir. Alias boşsa SQL'e `.role_id`
|
|
50
|
+
* gider, SqlKata onu `""."role_id"` diye tırnaklar ve Postgres **42601 zero-length delimited
|
|
51
|
+
* identifier** ile reddeder (crm koşumu B-2). Hiçbir yerde varsayılan atanmaz: `Microservice.cs:671`
|
|
52
|
+
* yalnız ŞEMA üretiminde `Entity.Slug`'a düşer, runtime yolu düşmez. Join'siz adımda hata çıkmaz —
|
|
53
|
+
* bu yüzden yalnız join varken doldurulur.
|
|
54
|
+
*
|
|
55
|
+
* Alias gövdeden türetilebilir: kök alanlar `path` taşımaz ve join alias'ı DEĞİLDİR; kalan tek
|
|
56
|
+
* alias entity slug'ıdır. Türetilemiyorsa dokunmayız, MS-013/WF-019 block'u devralır.
|
|
57
|
+
*/
|
|
58
|
+
function fillRootAlias(table, fixes) {
|
|
59
|
+
if (asObjects(table.joins).length === 0)
|
|
60
|
+
return;
|
|
61
|
+
if (String(table.alias ?? '').trim() !== '')
|
|
62
|
+
return;
|
|
63
|
+
const joinAliases = collectJoinAliases(table.joins, new Set());
|
|
64
|
+
const candidates = new Set();
|
|
65
|
+
for (const key of ['selectFields', 'returnFields', 'fields', 'filters']) {
|
|
66
|
+
for (const entry of asObjects(table[key])) {
|
|
67
|
+
const alias = String(entry.alias ?? '').trim();
|
|
68
|
+
if (!alias || joinAliases.has(alias))
|
|
69
|
+
continue;
|
|
70
|
+
if (String(entry.path ?? '').trim() !== '')
|
|
71
|
+
continue;
|
|
72
|
+
candidates.add(alias);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (candidates.size !== 1)
|
|
76
|
+
return;
|
|
77
|
+
const [alias] = [...candidates];
|
|
78
|
+
table.alias = alias;
|
|
79
|
+
fixes.push(`kök alias '${alias}' olarak dolduruldu — join'li table'da kök alias boşsa SQL'e '.<alan>' gider ve Postgres 42601 (zero-length delimited identifier) döner`);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Platform sort/limit'i null bırakır ve path'i türetmez; Studio ikisini de guard'sız okur.
|
|
83
|
+
* Eksik olanları doldurur, dolu olanlara DOKUNMAZ (yanlış değerleri MS-007 block'u yakalar).
|
|
84
|
+
*/
|
|
85
|
+
export function normalizeTableParameters(parameters, fallbackEntityId) {
|
|
86
|
+
if (!parameters || typeof parameters !== 'object')
|
|
87
|
+
return [];
|
|
88
|
+
const table = parameters;
|
|
89
|
+
const rootEntityId = String(table.entityId ?? fallbackEntityId ?? '');
|
|
90
|
+
const fixes = [];
|
|
91
|
+
if (!table.sort) {
|
|
92
|
+
table.sort = { ...SORT_DEFAULT };
|
|
93
|
+
fixes.push('sort varsayılanı eklendi');
|
|
94
|
+
}
|
|
95
|
+
if (!table.limit) {
|
|
96
|
+
table.limit = { ...LIMIT_DEFAULT };
|
|
97
|
+
fixes.push('limit varsayılanı eklendi');
|
|
98
|
+
}
|
|
99
|
+
fillRootAlias(table, fixes);
|
|
100
|
+
if (rootEntityId) {
|
|
101
|
+
const aliasChains = new Map();
|
|
102
|
+
fillJoinPaths(rootEntityId, table.joins, [], aliasChains, fixes);
|
|
103
|
+
fillSelectionPaths(table, aliasChains, fixes);
|
|
104
|
+
}
|
|
105
|
+
return fixes;
|
|
106
|
+
}
|
|
107
|
+
export function normalizeMicroserviceDefinition(definition) {
|
|
108
|
+
if (definition.source !== 'table')
|
|
109
|
+
return [];
|
|
110
|
+
return normalizeTableParameters(definition.sourceParameters, definition.entityId);
|
|
111
|
+
}
|
|
112
|
+
export function withNormalization(payload, fixes) {
|
|
113
|
+
if (fixes.length > 0) {
|
|
114
|
+
logStep('düzeltme', `gateway ${fixes.length} otomatik düzeltme uyguladı — gövdeye istek gönderilmeden ÖNCE işlendi, yanıta gatewayNormalized olarak eklenir`, fixes);
|
|
115
|
+
}
|
|
116
|
+
if (fixes.length === 0 || !payload || typeof payload !== 'object')
|
|
117
|
+
return payload;
|
|
118
|
+
return { ...payload, gatewayNormalized: fixes };
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Studio kenarı branch ADIYLA çizer (workflow.helpers.ts:90-110), condition tutamağını
|
|
122
|
+
* items[].branchId GUID'iyle. İkisi ancak branch.name = branch.id iken eşleşir; ad farklıysa
|
|
123
|
+
* runtime çalışır ama Studio'da koşul dalları kopuk görünür (klinik K-010). Tamamen
|
|
124
|
+
* türetilebilir olduğu için lint değil normalizasyon; kalan uyuşmazlığı WF-014 yakalar.
|
|
125
|
+
*/
|
|
126
|
+
function normalizeConditionBranches(step) {
|
|
127
|
+
const referenced = new Set(asObjects(step.rule?.items).map(item => String(item.branchId ?? '')));
|
|
128
|
+
const renamed = [];
|
|
129
|
+
for (const branch of asObjects(step.branches)) {
|
|
130
|
+
const branchId = String(branch.id ?? '');
|
|
131
|
+
if (!branchId || !referenced.has(branchId) || branch.name === branchId)
|
|
132
|
+
continue;
|
|
133
|
+
renamed.push(`${branch.name ?? '?'} → ${branchId}`);
|
|
134
|
+
branch.name = branchId;
|
|
135
|
+
}
|
|
136
|
+
return renamed;
|
|
137
|
+
}
|
|
138
|
+
const BEHAVIOR_VALUES = new Set(['primitive', 'object', 'array']);
|
|
139
|
+
/**
|
|
140
|
+
* `CreateWorkflowCommandValidator`: `When(ReturnDtoId.HasValue) → RuleFor(ReturnBehavior).NotEmpty()`
|
|
141
|
+
* (aynısı ParameterDtoId/ParameterBehavior için). DtoId verip Behavior'ı atlamak
|
|
142
|
+
* "'Return Behavior' must not be empty" ile ilk denemeyi kırar (crm koşumu B-5) — üstelik
|
|
143
|
+
* `ngapps://examples/middleware-authenticate` ve rehberin asgari iskeleti alanı göstermiyordu.
|
|
144
|
+
* Değer tamamen türetilebilir olmadığı için varsayılanı 'object' seçip yanıtta bildiriyoruz:
|
|
145
|
+
* DTO primitive ise `WorkflowService:445` WrongDtoBehaviorException ile net konuşur, dizi
|
|
146
|
+
* dönen akışlarda 'array' elle verilir.
|
|
147
|
+
*/
|
|
148
|
+
function fillDtoBehaviors(definition, fixes) {
|
|
149
|
+
const pairs = [['returnDtoId', 'returnBehavior'], ['parameterDtoId', 'parameterBehavior']];
|
|
150
|
+
for (const [dtoKey, behaviorKey] of pairs) {
|
|
151
|
+
const dtoId = definition[dtoKey];
|
|
152
|
+
const behavior = definition[behaviorKey];
|
|
153
|
+
if (typeof dtoId !== 'string' || dtoId.trim() === '')
|
|
154
|
+
continue;
|
|
155
|
+
if (typeof behavior === 'string' && BEHAVIOR_VALUES.has(behavior))
|
|
156
|
+
continue;
|
|
157
|
+
if (behavior !== undefined && behavior !== null && behavior !== '')
|
|
158
|
+
continue;
|
|
159
|
+
definition[behaviorKey] = 'object';
|
|
160
|
+
fixes.push(`${behaviorKey}: 'object' yazıldı — ${dtoKey} verilen gövdede zorunludur (validator: '${behaviorKey} must not be empty'). Akış DİZİ dönüyorsa 'array', DTO primitive ise 'primitive' verin`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
export function normalizeWorkflowDefinition(definition) {
|
|
164
|
+
const fixes = [];
|
|
165
|
+
const bodies = [definition, definition.automation];
|
|
166
|
+
fillDtoBehaviors(definition, fixes);
|
|
167
|
+
for (const body of bodies) {
|
|
168
|
+
if (!body || typeof body !== 'object')
|
|
169
|
+
continue;
|
|
170
|
+
for (const step of asObjects(body.steps)) {
|
|
171
|
+
const label = String(step.name ?? step.id ?? '?');
|
|
172
|
+
if (step.type === 'table') {
|
|
173
|
+
const applied = normalizeTableParameters(step.rule);
|
|
174
|
+
if (applied.length > 0)
|
|
175
|
+
fixes.push(`'${label}': ${applied.join(', ')}`);
|
|
176
|
+
}
|
|
177
|
+
if (step.type === 'condition') {
|
|
178
|
+
const renamed = normalizeConditionBranches(step);
|
|
179
|
+
if (renamed.length > 0) {
|
|
180
|
+
fixes.push(`'${label}': condition branch adları id'leriyle eşitlendi (${renamed.join(', ')}) — Studio kenarları branch adıyla çizer`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return fixes;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Sayfa akışında microservice adımının sonucu `machine.setStorage(task.name, …)` ile yazılır ve
|
|
189
|
+
* sarmalı `isRaw` belirler: false → `{ data, headers, error }`, aksi hâlde ham `response.data`
|
|
190
|
+
* (microservice.ts:81-90). Studio akışı her okuduğunda isRaw'ı false'a çekip storage ref'lerine
|
|
191
|
+
* '.data' eklediği (event-actions.mappers.ts:142-181) için tek kararlı bileşim isRaw=false +
|
|
192
|
+
* '<adım adı>.data'dır; verilmemiş isRaw'ı buna eşitliyoruz ki editörde bir kez açılan akış
|
|
193
|
+
* sessizce başka bir şeye dönüşmesin. Kalan tutarsızlığı FLOW-009 yakalar.
|
|
194
|
+
*/
|
|
195
|
+
export function normalizeFlowSteps(steps) {
|
|
196
|
+
const fixes = [];
|
|
197
|
+
for (const step of asObjects(steps)) {
|
|
198
|
+
if (step.type !== 'microservice')
|
|
199
|
+
continue;
|
|
200
|
+
const args = (step.args ?? {});
|
|
201
|
+
if (args.isRaw === null || args.isRaw === undefined) {
|
|
202
|
+
args.isRaw = false;
|
|
203
|
+
step.args = args;
|
|
204
|
+
fixes.push(`'${String(step.stepId ?? step.name ?? '?')}': args.isRaw=false yazıldı — storage { data, headers, error } sarmalıdır, setter ref'i '<adım adı>.data' olmalı`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return fixes;
|
|
208
|
+
}
|
|
209
|
+
const DEFAULT_STATES = { isSticky: false, isDroppable: false, isScrollable: false, isWrapper: false };
|
|
210
|
+
const REQUIRED_COLLECTIONS = ['events', 'validations', 'stateBindings', 'children'];
|
|
211
|
+
/**
|
|
212
|
+
* Komponent gövdesinde eksik bırakılan alanlar platformda NullReferenceException 500 üretir:
|
|
213
|
+
*
|
|
214
|
+
* - `states`: DesignComponentManagementSchema.ToDomain:47 `States.IsSticky`'yi guard'sız okur;
|
|
215
|
+
* Appearance'ın satır 24'te varsayılanı var, States'in yok.
|
|
216
|
+
* - `events` / `validations` / `stateBindings`: yalnız **micro-op yolunda** zorunlu.
|
|
217
|
+
* `page_create` (POST /pages) bunlarsız 200 döner, `PATCH …/schema` NRE verir; canlı deneyde
|
|
218
|
+
* üçü birlikte verilince geçti, her biri tek başına verilince patladı (klinik H-005 eki).
|
|
219
|
+
*
|
|
220
|
+
* Hiçbiri `page_schema_validate` tarafından yakalanmaz (ValidatePageSchemaQuery ToDomain'e
|
|
221
|
+
* girmez) — doğrulayıcı "valid" der, yazma patlar. Tamamen türetilebilir olduğu için lint
|
|
222
|
+
* değil normalizasyon.
|
|
223
|
+
*/
|
|
224
|
+
const PROP_VALUE_TYPES = new Set(['static', 'file', 'flow', 'widget', 'element', 'page', 'code']);
|
|
225
|
+
function isPropShaped(value) {
|
|
226
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value) && PROP_VALUE_TYPES.has(String(value.type));
|
|
227
|
+
}
|
|
228
|
+
function wrapPropDictionaryValues(dictionary) {
|
|
229
|
+
let wrapped = false;
|
|
230
|
+
for (const key of Object.keys(dictionary)) {
|
|
231
|
+
const value = dictionary[key];
|
|
232
|
+
if (isPropShaped(value)) {
|
|
233
|
+
wrapped = deepWrapPropData(value.data) || wrapped;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
const prop = { type: 'static', ref: null, data: value };
|
|
237
|
+
dictionary[key] = prop;
|
|
238
|
+
deepWrapPropData(prop.data);
|
|
239
|
+
wrapped = true;
|
|
240
|
+
}
|
|
241
|
+
return wrapped;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Platform, bir Prop'un data'sı JSON nesne/diziyse içini HER SEVİYEDE Dictionary<string, Prop>
|
|
245
|
+
* olarak çözer (DesignComponentManagementSchema.CreateProp) — düz bırakılan iç değer
|
|
246
|
+
* JsonException verir (kutuphane turu H5: header.navigation icon'u). Dolu ve doğru şekle
|
|
247
|
+
* dokunulmaz; yalnız Prop olmayan iç değerler static Prop'a sarılır.
|
|
248
|
+
*/
|
|
249
|
+
export function deepWrapPropData(data) {
|
|
250
|
+
if (Array.isArray(data)) {
|
|
251
|
+
let wrapped = false;
|
|
252
|
+
for (const item of data) {
|
|
253
|
+
if (isPropShaped(item)) {
|
|
254
|
+
wrapped = deepWrapPropData(item.data) || wrapped;
|
|
255
|
+
}
|
|
256
|
+
else if (item && typeof item === 'object') {
|
|
257
|
+
wrapped = wrapPropDictionaryValues(item) || wrapped;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return wrapped;
|
|
261
|
+
}
|
|
262
|
+
if (data && typeof data === 'object') {
|
|
263
|
+
return wrapPropDictionaryValues(data);
|
|
264
|
+
}
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* `props` sözlüğünün DEĞERLERİ de Prop'tur (`Dictionary<string, Prop>`); sarmalsız bırakılan bir
|
|
269
|
+
* prop hata bile vermez, **sessizce boş kaydedilir**: `"style": { "color": "#f00" }` gövdesinde
|
|
270
|
+
* Newtonsoft `Prop`'un hiçbir alanına denk gelmeyen `color`'ı atar, Type varsayılana (`static`)
|
|
271
|
+
* düşer, Data null kalır — canlı klinikte `style` prop'u `{"type":"static"}` olarak kaydedildi
|
|
272
|
+
* (S-001). Primitive bırakılırsa (`"content": "merhaba"`) bu kez deserialize aşamasında 500 gelir.
|
|
273
|
+
* İkisi de tamamen türetilebilir olduğu için lint değil normalizasyon; Studio'nun
|
|
274
|
+
* `mapRawPropToSchemaProp`'u (screen-builder/mappers.ts:131) tel üzerinde aynı sarmalı kuruyor.
|
|
275
|
+
*/
|
|
276
|
+
function wrapNodeProps(props) {
|
|
277
|
+
const wrapped = [];
|
|
278
|
+
const deepened = [];
|
|
279
|
+
for (const [name, value] of Object.entries(props)) {
|
|
280
|
+
if (value === null || value === undefined)
|
|
281
|
+
continue;
|
|
282
|
+
if (isPropShaped(value)) {
|
|
283
|
+
if (deepWrapPropData(value.data))
|
|
284
|
+
deepened.push(name);
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
const prop = { type: 'static', ref: null, data: value };
|
|
288
|
+
props[name] = prop;
|
|
289
|
+
deepWrapPropData(prop.data);
|
|
290
|
+
wrapped.push(name);
|
|
291
|
+
}
|
|
292
|
+
return { wrapped, deepened };
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Validation gövdesi ÜÇ farklı biçimde dolaşıyordu ve ikisi bozuktu (crm koşumu B-9):
|
|
296
|
+
*
|
|
297
|
+
* - `{ name, value, message }` (eski altın örnek) → `ValidationModel.Name` **JsonIgnore**'dur,
|
|
298
|
+
* `Type` enum varsayılanına (`Regex`) düşer: alan sessizce regex doğrulamasına dönüşür.
|
|
299
|
+
* - `{ type: 'maxLength' }` → backend eski enum üyesiyle (`OldMaxLength`) kabul eder ve geri
|
|
300
|
+
* okur, ama RUNTIME doğrulayıcı yalnız tireli adları tanır
|
|
301
|
+
* (`ng-apps-flow/src/tasks/validation/validation.ts:74` switch → default: return true):
|
|
302
|
+
* doğrulama hiç koşmaz. Bu yüzden eski adları kanonik karşılığına çeviriyoruz.
|
|
303
|
+
* - `message` boş bırakılan validation kullanıcıya hiçbir şey söylemez; doküman 23'te sayfayı
|
|
304
|
+
* okunamaz bırakan gövde de buydu. Türetilemez → normalizasyon değil, PAGE-016 block'u.
|
|
305
|
+
*/
|
|
306
|
+
const VALIDATION_CANONICAL_TYPES = ['regex', 'email', 'required', 'max-length', 'min-length', 'max-value', 'min-value', 'value-options'];
|
|
307
|
+
const VALIDATION_TYPE_ALIASES = new Map([
|
|
308
|
+
...VALIDATION_CANONICAL_TYPES.map(type => [type.replace(/-/g, ''), type]),
|
|
309
|
+
['pattern', 'regex'],
|
|
310
|
+
['min', 'min-value'],
|
|
311
|
+
['max', 'max-value']
|
|
312
|
+
]);
|
|
313
|
+
const VALIDATION_VALUE_REQUIRED = new Set(['regex', 'max-length', 'min-length', 'max-value', 'min-value', 'value-options']);
|
|
314
|
+
function canonicalValidationType(value) {
|
|
315
|
+
const key = String(value ?? '').trim().toLowerCase().replace(/[-_\s]/g, '');
|
|
316
|
+
return key === '' ? undefined : VALIDATION_TYPE_ALIASES.get(key);
|
|
317
|
+
}
|
|
318
|
+
export function normalizeValidationList(validations, label, fixes) {
|
|
319
|
+
for (const validation of asObjects(validations)) {
|
|
320
|
+
const declared = validation.type ?? validation.name;
|
|
321
|
+
const canonical = canonicalValidationType(declared);
|
|
322
|
+
const name = `${label}.${String(declared ?? '?')}`;
|
|
323
|
+
if (canonical) {
|
|
324
|
+
if (validation.type !== canonical) {
|
|
325
|
+
fixes.push(validation.type === undefined
|
|
326
|
+
? `${name}: 'name' anahtarı 'type: ${canonical}' olarak yazıldı — Name alanı JsonIgnore'dur, type verilmezse validation sessizce regex'e düşer`
|
|
327
|
+
: `${name}: validation tipi '${canonical}' olarak yazıldı — runtime doğrulayıcı yalnız tireli adı tanır, eskisi sessizce hiçbir şey doğrulamaz`);
|
|
328
|
+
validation.type = canonical;
|
|
329
|
+
}
|
|
330
|
+
delete validation.name;
|
|
331
|
+
}
|
|
332
|
+
if (typeof validation.isActive !== 'boolean') {
|
|
333
|
+
validation.isActive = true;
|
|
334
|
+
fixes.push(`${name}: isActive=true yazıldı`);
|
|
335
|
+
}
|
|
336
|
+
if (canonical === 'required' && (validation.value === undefined || validation.value === null)) {
|
|
337
|
+
validation.value = true;
|
|
338
|
+
fixes.push(`${name}: value=true yazıldı (required)`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Normalizasyondan SONRA kalan, türetilemeyen eksikler: tanınmayan tip (sessizce regex'e düşer)
|
|
344
|
+
* ve boş mesaj (kullanıcı neyin yanlış olduğunu göremez). Değer isteyen tiplerde eksik `value`'yu
|
|
345
|
+
* platform `missing-validation-value` ile yakalıyor, biz de yazmadan önce söylüyoruz.
|
|
346
|
+
*/
|
|
347
|
+
export function validationErrors(validations, label) {
|
|
348
|
+
const errors = [];
|
|
349
|
+
for (const validation of asObjects(validations)) {
|
|
350
|
+
const declared = validation.type ?? validation.name;
|
|
351
|
+
const canonical = canonicalValidationType(declared);
|
|
352
|
+
const name = `${label}.${String(declared ?? '?')}`;
|
|
353
|
+
if (!canonical) {
|
|
354
|
+
errors.push(`${name}: geçersiz validation tipi — geçerliler: ${VALIDATION_CANONICAL_TYPES.join(' · ')}. Tip verilmeyen ya da tanınmayan validation platformda sessizce 'regex'e düşer.`);
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
if (typeof validation.message !== 'string' || validation.message.trim() === '') {
|
|
358
|
+
errors.push(`${name}: 'message' zorunlu — mesajsız validation kullanıcıya hiçbir şey söylemez ve eksik alanlı validation gövdesi sayfayı okunamaz bırakabilir (doküman 23 §B-2).`);
|
|
359
|
+
}
|
|
360
|
+
if (VALIDATION_VALUE_REQUIRED.has(canonical) && (validation.value === undefined || validation.value === null || validation.value === '')) {
|
|
361
|
+
errors.push(`${name}: '${canonical}' tipi 'value' ister (uzunluk/sınır/regex deseni); platform bunu 'missing-validation-value' ile reddeder.`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return errors;
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* add_component ağacındaki ve set_validations op'larındaki validation gövdelerini birlikte
|
|
368
|
+
* denetler; page_schema_patch/validate yolunda PAGE-016 mesajını buradan üretiyoruz.
|
|
369
|
+
*/
|
|
370
|
+
export function schemaOpValidationErrors(ops) {
|
|
371
|
+
const errors = [];
|
|
372
|
+
const walkNodes = (nodes) => {
|
|
373
|
+
for (const node of asObjects(nodes)) {
|
|
374
|
+
errors.push(...validationErrors(node.validations, String(node.elementId ?? node.name ?? node.type ?? '?')));
|
|
375
|
+
walkNodes(node.children);
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
for (const op of asObjects(ops)) {
|
|
379
|
+
if (op.op === 'set_validations') {
|
|
380
|
+
errors.push(...validationErrors(op.validations, String(op.target ?? '?')));
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
if (op.op === 'add_component' && op.component)
|
|
384
|
+
walkNodes([op.component]);
|
|
385
|
+
}
|
|
386
|
+
return errors;
|
|
387
|
+
}
|
|
388
|
+
export function normalizeComponentDefaults(nodes) {
|
|
389
|
+
const fixed = [];
|
|
390
|
+
for (const node of asObjects(nodes)) {
|
|
391
|
+
const missing = [];
|
|
392
|
+
if (!node.states || typeof node.states !== 'object') {
|
|
393
|
+
node.states = { ...DEFAULT_STATES };
|
|
394
|
+
missing.push('states');
|
|
395
|
+
}
|
|
396
|
+
for (const key of REQUIRED_COLLECTIONS) {
|
|
397
|
+
if (Array.isArray(node[key]))
|
|
398
|
+
continue;
|
|
399
|
+
node[key] = [];
|
|
400
|
+
missing.push(key);
|
|
401
|
+
}
|
|
402
|
+
if (node.props && typeof node.props === 'object' && !Array.isArray(node.props)) {
|
|
403
|
+
const { wrapped, deepened } = wrapNodeProps(node.props);
|
|
404
|
+
if (wrapped.length > 0)
|
|
405
|
+
missing.push(`prop'lar Prop sarmalına alındı: ${wrapped.join('+')}`);
|
|
406
|
+
if (deepened.length > 0)
|
|
407
|
+
missing.push(`iç değerler Prop'a sarıldı: ${deepened.join('+')}`);
|
|
408
|
+
}
|
|
409
|
+
normalizeValidationList(node.validations, 'validations', missing);
|
|
410
|
+
if (missing.length > 0) {
|
|
411
|
+
fixed.push(`${String(node.elementId ?? node.name ?? node.type ?? '?')} (${missing.join(', ')})`);
|
|
412
|
+
}
|
|
413
|
+
fixed.push(...normalizeComponentDefaults(node.children));
|
|
414
|
+
}
|
|
415
|
+
return fixed;
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* add_component micro-op'unun taşıdığı komponent ağacı da aynı ToDomain yolundan geçer.
|
|
419
|
+
*/
|
|
420
|
+
/**
|
|
421
|
+
* `replace_prop` micro-op'u Prop DEĞİL ham değer alır: PageSchemaMicroOpApplier.cs:77
|
|
422
|
+
* `new Prop(PropValueType.Static) { Data = op.Value }` kurar (ifade verilmişse Code). Prop sarmalı
|
|
423
|
+
* gönderildiğinde sarmalın kendisi `Data`'ya yazılır ve platform onu `Dictionary<string, Prop>`
|
|
424
|
+
* olarak çözmeye çalışıp **500 JsonException** verir ("could not be converted to Prop. Path: $.type").
|
|
425
|
+
* add_component'te sarmal ZORUNLU olduğu için bu tuzağa düşmek çok kolay; şekil tamamen
|
|
426
|
+
* türetilebilir olduğundan lint değil normalizasyon: static sarmalını açıyor, code sarmalını
|
|
427
|
+
* op'un `expression` alanına taşıyoruz. Diğer prop tipleri (page/file/flow/widget/element) bu
|
|
428
|
+
* op'la ifade EDİLEMEZ — onlar için PAGE-006 uyarısı üretilir.
|
|
429
|
+
*
|
|
430
|
+
* DIŞ sarmalın olmaması İÇ sarmalın da olmaması demek değil: applier'ın kurduğu Prop yine
|
|
431
|
+
* `ToDomain` → `CreateProp` yolundan geçer ve `Data`'nın içi HER SEVİYEDE
|
|
432
|
+
* `Dictionary<string, Prop>` olarak çözülür. Klinikte iki biçim de 500 verdi (S-001):
|
|
433
|
+
* ham `{"backgroundColor":"#f8fafc"}` → `Path: $.backgroundColor`, dış sarmallı
|
|
434
|
+
* `{"type":"static","data":{"color":"#111827"}}` → `Path: $.color`. Geçen tek biçim,
|
|
435
|
+
* Studio'nun `mapRawPropToSchemaProp` çıktısının aynısı: dış sarmal YOK, her yaprak sarılı.
|
|
436
|
+
*/
|
|
437
|
+
function normalizeReplacePropValue(op) {
|
|
438
|
+
const value = op.value;
|
|
439
|
+
const label = `'${String(op.target ?? '?')}.${String(op.prop ?? '?')}'`;
|
|
440
|
+
if (!isPropShaped(value)) {
|
|
441
|
+
if (!deepWrapPropData(value))
|
|
442
|
+
return [];
|
|
443
|
+
return [`${label}: value'nun iç değerleri Prop'a sarıldı — op ham değer alır ama platform Data'nın içini her seviyede Dictionary<string, Prop> olarak çözer, sarımsız iç değer 500 JsonException verir`];
|
|
444
|
+
}
|
|
445
|
+
const propType = String(value.type);
|
|
446
|
+
if (propType === 'static') {
|
|
447
|
+
const data = value.data;
|
|
448
|
+
op.value = data;
|
|
449
|
+
const notes = [`${label}: replace_prop value'su Prop sarmalından çıkarıldı — bu op ham değer alır, sarmal gönderilirse platform 500 JsonException verir`];
|
|
450
|
+
if (deepWrapPropData(data))
|
|
451
|
+
notes.push(`${label}: value'nun iç değerleri Prop'a sarıldı`);
|
|
452
|
+
return notes;
|
|
453
|
+
}
|
|
454
|
+
if (propType === 'code') {
|
|
455
|
+
const expression = value.data ?? value.ref;
|
|
456
|
+
delete op.value;
|
|
457
|
+
op.expression = expression;
|
|
458
|
+
return [`${label}: replace_prop code prop'u 'expression' alanına taşındı — kod prop'u value ile değil expression ile verilir`];
|
|
459
|
+
}
|
|
460
|
+
return [];
|
|
461
|
+
}
|
|
462
|
+
export function normalizeSchemaOps(ops) {
|
|
463
|
+
const components = [];
|
|
464
|
+
const props = [];
|
|
465
|
+
const validations = [];
|
|
466
|
+
for (const op of asObjects(ops)) {
|
|
467
|
+
if (op.op === 'replace_prop') {
|
|
468
|
+
props.push(...normalizeReplacePropValue(op));
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
if (op.op === 'set_validations') {
|
|
472
|
+
normalizeValidationList(op.validations, `'${String(op.target ?? '?')}'`, validations);
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
if (op.op !== 'add_component' || !op.component)
|
|
476
|
+
continue;
|
|
477
|
+
components.push(...normalizeComponentDefaults([op.component]));
|
|
478
|
+
}
|
|
479
|
+
return { components, props, validations };
|
|
480
|
+
}
|
|
481
|
+
export function validationNote(fixed) {
|
|
482
|
+
return fixed.length === 0 ? [] : [`validation gövdeleri düzeltildi (${fixed.join(' · ')})`];
|
|
483
|
+
}
|
|
484
|
+
export function replacePropNote(fixed) {
|
|
485
|
+
return fixed.length === 0 ? [] : [`replace_prop değerleri düzeltildi (${fixed.join(' · ')})`];
|
|
486
|
+
}
|
|
487
|
+
export const PAGE_CONTAINER_ELEMENT_ID = 'page_container';
|
|
488
|
+
/**
|
|
489
|
+
* Platformun kendi açtığı sayfalarda (CreatePageWhenApplicationCreatedEventHandler) şemanın kökünde
|
|
490
|
+
* her zaman bir `page_container` bulunur; Studio komponentleri onun children'ına koyar ve sayfa
|
|
491
|
+
* düzeyi olayları (page load) ona bağlar. page_create ile açılan sayfa boş şemayla geldiği için
|
|
492
|
+
* ajan komponentleri köke serpiyor — kapsayıcı dışında kalıyorlar. Şekil qm/klinik'teki gerçek
|
|
493
|
+
* kayıttan alındı.
|
|
494
|
+
*
|
|
495
|
+
* Düzen prop'ları da kurulur (2026-08-25): `ContainerComponent` şemasının `layout` varsayılanı
|
|
496
|
+
* `grid` → `flex` oldu ve Studio bırakma yolu katalogdaki `definition.defaults`'u düğümün
|
|
497
|
+
* props'una YAZAR (`ScreenBuilderDroppable.svelte`); sayfa kökünü kuran öbür üreticiler de aynı seti
|
|
498
|
+
* yazar (Studio `DEFAULT_CONTAINER_LAYOUT_PROPS`, studio-api `generatePageContainer`, platform
|
|
499
|
+
* `CreatePageWhenApplicationCreatedEventHandler` + `SeedData`). Renderer varsayılanı
|
|
500
|
+
* BİLMEZ — `isFlexLayout` yalnız düğümün kendi prop'una bakar — bu yüzden gateway'in kurduğu
|
|
501
|
+
* kapsayıcı da seti açıkça taşır; yoksa Studio'dan açılan sayfa flex, MCP'den açılan ızgara olurdu.
|
|
502
|
+
*/
|
|
503
|
+
export function pageContainerNode() {
|
|
504
|
+
return {
|
|
505
|
+
elementId: PAGE_CONTAINER_ELEMENT_ID,
|
|
506
|
+
kind: 'component',
|
|
507
|
+
name: PAGE_CONTAINER_ELEMENT_ID,
|
|
508
|
+
type: 'container',
|
|
509
|
+
props: {
|
|
510
|
+
title: { type: 'static', data: '' },
|
|
511
|
+
layout: { type: 'static', data: 'flex' },
|
|
512
|
+
direction: { type: 'static', data: 'row' },
|
|
513
|
+
horizontalAlign: { type: 'static', data: 'left' },
|
|
514
|
+
verticalAlign: { type: 'static', data: 'top' },
|
|
515
|
+
size: { type: 'static', data: 'shrink' },
|
|
516
|
+
gap: { type: 'static', data: 'medium' }
|
|
517
|
+
},
|
|
518
|
+
blocks: [{ breakpoint: 1024, size: { h: 24, w: 12 }, position: { x: 0, y: 0 } }],
|
|
519
|
+
states: { isSticky: false, isDroppable: true, isScrollable: true, isWrapper: true },
|
|
520
|
+
appearance: { heightType: 'auto', isHidden: false, marginValue: 0, marginType: 'none', paddingType: 'none', paddingValue: 0 },
|
|
521
|
+
events: [],
|
|
522
|
+
validations: [],
|
|
523
|
+
stateBindings: [],
|
|
524
|
+
children: []
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* `modal` ve `drawer` bu kuralın istisnasıdır: qm'in gerçek kayıtlarında (`[SY] Profil bilgilerim`)
|
|
529
|
+
* katmanlar `page_container`'ın İÇİNDE değil, şemanın kökünde onun kardeşi olarak durur. Kapsayıcıya
|
|
530
|
+
* çekilirlerse sayfa içeriğinin akışına girer ve katman olarak açılmazlar.
|
|
531
|
+
*/
|
|
532
|
+
const ROOT_LAYER_TYPES = new Set(['modal', 'drawer']);
|
|
533
|
+
export function ensurePageContainer(schema) {
|
|
534
|
+
const nodes = asObjects(schema);
|
|
535
|
+
if (nodes.some(node => node.elementId === PAGE_CONTAINER_ELEMENT_ID))
|
|
536
|
+
return { schema: nodes, fixes: [] };
|
|
537
|
+
const container = pageContainerNode();
|
|
538
|
+
const layers = nodes.filter(node => ROOT_LAYER_TYPES.has(String(node.type ?? '')));
|
|
539
|
+
container.children = nodes.filter(node => !ROOT_LAYER_TYPES.has(String(node.type ?? '')));
|
|
540
|
+
const moved = container.children.length;
|
|
541
|
+
return {
|
|
542
|
+
schema: [container, ...layers],
|
|
543
|
+
fixes: [`şemaya '${PAGE_CONTAINER_ELEMENT_ID}' kapsayıcısı eklendi${moved > 0 ? ` ve verdiğiniz ${moved} komponent onun içine alındı` : ''}${layers.length > 0 ? `; ${layers.length} katman (modal/drawer) kökte kapsayıcının kardeşi olarak bırakıldı` : ''}: sayfa komponentleri köke değil bu kapsayıcıya konur, sayfa düzeyi olaylar (page load) da ona bağlanır`]
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Komponent kataloğunda varsayılan `size`'ı kendi `maxSize`'ını aşan kayıtlar var (`textinput`
|
|
548
|
+
* default `h:8` / `maxSize.h:5` — P-07 taramasında 19 komponent, NgApps.UI ARC-013). Ajan
|
|
549
|
+
* `component_get`'teki varsayılanı kopyaladığında doğrulayıcı "Height of 'x' must be between 1
|
|
550
|
+
* and 5." ile reddediyor; form sayfaları baştan input başına bir hataya düşüyor. Şekil tamamen
|
|
551
|
+
* türetilebilir olduğu için lint değil normalizasyon: aşan değeri katalogdaki sınıra çekiyoruz.
|
|
552
|
+
*/
|
|
553
|
+
export function clampBlockSizes(nodes, maxSizes) {
|
|
554
|
+
const clamped = [];
|
|
555
|
+
for (const node of asObjects(nodes)) {
|
|
556
|
+
const limit = maxSizes.get(String(node.type ?? ''));
|
|
557
|
+
for (const block of limit ? asObjects(node.blocks) : []) {
|
|
558
|
+
const size = block.size;
|
|
559
|
+
if (!size || typeof size !== 'object')
|
|
560
|
+
continue;
|
|
561
|
+
for (const axis of ['w', 'h']) {
|
|
562
|
+
const value = size[axis];
|
|
563
|
+
const max = limit?.[axis];
|
|
564
|
+
if (typeof value !== 'number' || typeof max !== 'number' || value <= max)
|
|
565
|
+
continue;
|
|
566
|
+
size[axis] = max;
|
|
567
|
+
clamped.push(`${String(node.elementId ?? node.type)} ${axis}:${value}→${max}`);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
clamped.push(...clampBlockSizes(node.children, maxSizes));
|
|
571
|
+
}
|
|
572
|
+
return clamped;
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* Gövdede `appearance` verilmediğinde platform yüksekliği **fixed**'e sabitler:
|
|
576
|
+
* `DesignComponentManagementSchema.cs:24` alanı `new(ComponentHeightType.Fixed, …)` ile
|
|
577
|
+
* başlatır (`ComponentHeightType.Fixed = 0`), yani "alan yok" = "sabit yükseklik". Studio'nun
|
|
578
|
+
* kendi bırakma yolu bunu ASLA yapmaz: katalogdaki `isDefaultAutoHeight`e bakıp
|
|
579
|
+
* `heightType: isDefaultAutoHeight ? 'auto' : 'fixed'` yazar
|
|
580
|
+
* (`ScreenBuilderDroppable.svelte:701`, `store-converter.ts:233`). Katalogda varsayılan
|
|
581
|
+
* `true`'dur (`ZeyrekComponent.ts:62`); `false` diyen yalnız sekiz tip var — auth-layout, html,
|
|
582
|
+
* image, layout, qrcode, table, text-editor, video.
|
|
583
|
+
*
|
|
584
|
+
* Fark tuvalde görünür ve yıkıcıdır: `fixed` bir düğüm tam `size.h * 10px` çizilir ve
|
|
585
|
+
* `.draggable__item-component { overflow: hidden }` taşan içeriği KESER
|
|
586
|
+
* (`DraggableItem.svelte`). `auto` düğümde yükseklik içerikten gelir, ResizeObserver ölçtüğünü
|
|
587
|
+
* `shrinkOrExpand` ile `blocks[].size.h`e geri yazar ve kardeşleri iter. Yani MCP hattında
|
|
588
|
+
* ajanın tahmin ettiği `h` bir üst SINIRA dönüşüyor, kartların içi kırpılmış görünüyordu
|
|
589
|
+
* (ürün sahibi ekran görüntüsü, 2026-08-19: randevu sayfasında üç kartın da içi kesik).
|
|
590
|
+
*
|
|
591
|
+
* Ajanın açıkça verdiği `appearance.heightType` korunur; katalogda bulunmayan tip auto sayılır —
|
|
592
|
+
* platformun kendi varsayılanı odur ve `fixed` tahmininin bedeli kırpma, `auto` tahmininin
|
|
593
|
+
* bedeli yalnız fazladan yükseklik.
|
|
594
|
+
*/
|
|
595
|
+
export function normalizeHeightTypes(nodes, autoHeights) {
|
|
596
|
+
const stamped = [];
|
|
597
|
+
for (const node of asObjects(nodes)) {
|
|
598
|
+
const appearance = (node.appearance && typeof node.appearance === 'object' && !Array.isArray(node.appearance)
|
|
599
|
+
? node.appearance
|
|
600
|
+
: {});
|
|
601
|
+
if (appearance.heightType !== 'auto' && appearance.heightType !== 'fixed') {
|
|
602
|
+
const heightType = autoHeights.get(String(node.type ?? '')) === false ? 'fixed' : 'auto';
|
|
603
|
+
appearance.heightType = heightType;
|
|
604
|
+
node.appearance = appearance;
|
|
605
|
+
stamped.push(`${String(node.elementId ?? node.name ?? node.type ?? '?')}: ${heightType}`);
|
|
606
|
+
}
|
|
607
|
+
stamped.push(...normalizeHeightTypes(node.children, autoHeights));
|
|
608
|
+
}
|
|
609
|
+
return stamped;
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Kalabalık şemada düğüm başına satır yazmak notu okunmaz kılıyor; asıl bilgi KAÇ düğüme
|
|
613
|
+
* dokunulduğu ve hangilerinin `fixed` kaldığıdır — kırpma riski yalnız onlarda var.
|
|
614
|
+
*/
|
|
615
|
+
export function heightTypeNote(stamped) {
|
|
616
|
+
if (stamped.length === 0)
|
|
617
|
+
return [];
|
|
618
|
+
const fixed = stamped.filter(entry => entry.endsWith(': fixed')).map(entry => entry.replace(': fixed', ''));
|
|
619
|
+
const detail = stamped.length > 6
|
|
620
|
+
? `${stamped.length} düğüm${fixed.length > 0 ? `; katalog gereği fixed kalan: ${fixed.join(', ')}` : ''}`
|
|
621
|
+
: stamped.join(', ');
|
|
622
|
+
return [`appearance.heightType yazıldı (${detail}): platform boş bırakılan appearance'ı 'fixed'e sabitliyor, Studio ise katalogdaki isDefaultAutoHeight'a bakıyor. 'fixed' düğüm tuvalde tam size.h*10px çizilip taşan içeriği kırptığı için katalog varsayılanı gövdeye açıkça yazıldı; bilerek sınırlamak istediğin düğüme appearance.heightType:'fixed' ver`];
|
|
623
|
+
}
|
|
624
|
+
const GRID_COLUMNS = 12;
|
|
625
|
+
const DESKTOP_BREAKPOINT = 1024;
|
|
626
|
+
const DEFAULT_RECT = { x: 0, y: 0, w: 12, h: 6 };
|
|
627
|
+
const MAX_GRID_ROWS = 2000;
|
|
628
|
+
function coordinate(value, fallback) {
|
|
629
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
|
630
|
+
}
|
|
631
|
+
function desktopBlock(node) {
|
|
632
|
+
return asObjects(node.blocks).find(block => Number(block.breakpoint) === DESKTOP_BREAKPOINT);
|
|
633
|
+
}
|
|
634
|
+
function rectOf(block) {
|
|
635
|
+
const position = (block?.position ?? {});
|
|
636
|
+
const size = (block?.size ?? {});
|
|
637
|
+
return {
|
|
638
|
+
x: coordinate(position.x, DEFAULT_RECT.x),
|
|
639
|
+
y: coordinate(position.y, DEFAULT_RECT.y),
|
|
640
|
+
w: coordinate(size.w, DEFAULT_RECT.w),
|
|
641
|
+
h: coordinate(size.h, DEFAULT_RECT.h)
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
function occupancy(items, ignoredElementId) {
|
|
645
|
+
const rows = Math.min(MAX_GRID_ROWS, Math.max(...items.map(item => item.rect.y + item.rect.h), 1));
|
|
646
|
+
const matrix = Array.from({ length: rows }, () => new Array(GRID_COLUMNS).fill(undefined));
|
|
647
|
+
for (const item of items) {
|
|
648
|
+
if (item.elementId === ignoredElementId)
|
|
649
|
+
continue;
|
|
650
|
+
const width = Math.min(GRID_COLUMNS, item.rect.w);
|
|
651
|
+
const lastColumn = Math.min(GRID_COLUMNS, item.rect.x + width);
|
|
652
|
+
for (let row = Math.max(0, item.rect.y); row < item.rect.y + item.rect.h; row++) {
|
|
653
|
+
const cells = matrix[row];
|
|
654
|
+
if (!cells)
|
|
655
|
+
continue;
|
|
656
|
+
for (let column = Math.max(0, item.rect.x); column < lastColumn; column++) {
|
|
657
|
+
cells[column] = item.elementId;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
return matrix;
|
|
662
|
+
}
|
|
663
|
+
function overlappingIds(matrix, active) {
|
|
664
|
+
const width = Math.min(GRID_COLUMNS, active.rect.w);
|
|
665
|
+
const found = new Set();
|
|
666
|
+
for (const cells of matrix.slice(Math.max(0, active.rect.y), active.rect.y + active.rect.h)) {
|
|
667
|
+
for (const elementId of cells.slice(Math.max(0, active.rect.x), active.rect.x + width)) {
|
|
668
|
+
if (elementId && elementId !== active.elementId)
|
|
669
|
+
found.add(elementId);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
return [...found];
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* `findSiblingsForMove` birebir portu (ng-apps-screen-builder/draggable.element.ts): aktif
|
|
676
|
+
* komponentin kapladığı hücreleri işgal eden kardeşler onun ALTINA taşınır, taşınan kardeşin
|
|
677
|
+
* kendi çakışmaları da aynı mesafeyle zincirleme itilir. `visited` zincirin aynı komponente iki
|
|
678
|
+
* kez girmesini engeller; `nestedDifference`/`distance` üst seviyede ölçülen kaymayı korur ki
|
|
679
|
+
* yığın açılırken kardeşler arası boşluk büyümesin.
|
|
680
|
+
*/
|
|
681
|
+
function pushOverlappingDown(items, active, visited, nestedDifference = 0) {
|
|
682
|
+
const data = items.map(item => ({ elementId: item.elementId, rect: { ...item.rect } }));
|
|
683
|
+
const overlapping = overlappingIds(occupancy(data, active.elementId), active)
|
|
684
|
+
.map(elementId => data.find(item => item.elementId === elementId))
|
|
685
|
+
.filter((item) => item !== undefined)
|
|
686
|
+
.sort((left, right) => left.rect.y - right.rect.y);
|
|
687
|
+
const moved = [];
|
|
688
|
+
const bottom = active.rect.y + active.rect.h;
|
|
689
|
+
let previousElementId = active.elementId;
|
|
690
|
+
let distance = 0;
|
|
691
|
+
for (const item of overlapping) {
|
|
692
|
+
const wasVisited = visited.has(item.elementId);
|
|
693
|
+
const previousY = item.rect.y;
|
|
694
|
+
visited.add(item.elementId);
|
|
695
|
+
if (bottom === previousY)
|
|
696
|
+
continue;
|
|
697
|
+
item.rect.y = distance !== 0
|
|
698
|
+
? previousY + distance
|
|
699
|
+
: nestedDifference !== 0
|
|
700
|
+
? previousY + nestedDifference
|
|
701
|
+
: bottom;
|
|
702
|
+
distance = item.rect.y - previousY;
|
|
703
|
+
moved.push(item);
|
|
704
|
+
if (wasVisited)
|
|
705
|
+
continue;
|
|
706
|
+
moved.push(...pushOverlappingDown(data.filter(entry => entry.elementId !== previousElementId), item, visited, nestedDifference !== 0 ? nestedDifference : distance));
|
|
707
|
+
previousElementId = item.elementId;
|
|
708
|
+
}
|
|
709
|
+
return moved;
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* `moveItem` portu: aktif komponent kendi yerinde kalır (`updateItem` sonda onu geri yazar), yalnız
|
|
713
|
+
* itilen kardeşlerin ilk sonucu uygulanır — aynı kardeş zincirin farklı dallarından birden çok kez
|
|
714
|
+
* dönebilir ve ilki geçerlidir.
|
|
715
|
+
*/
|
|
716
|
+
function moveActive(items, active) {
|
|
717
|
+
const moved = pushOverlappingDown(items, active, new Set());
|
|
718
|
+
if (moved.length === 0)
|
|
719
|
+
return items;
|
|
720
|
+
return items.map(item => (item.elementId === active.elementId
|
|
721
|
+
? item
|
|
722
|
+
: moved.find(entry => entry.elementId === item.elementId) ?? item));
|
|
723
|
+
}
|
|
724
|
+
function reorderSiblings(nodes) {
|
|
725
|
+
const laidOut = nodes
|
|
726
|
+
.map((node, index) => ({ node, elementId: typeof node.elementId === 'string' && node.elementId !== '' ? node.elementId : `#${index}` }))
|
|
727
|
+
.filter(entry => !ROOT_LAYER_TYPES.has(String(entry.node.type ?? '')));
|
|
728
|
+
if (laidOut.length < 2)
|
|
729
|
+
return 0;
|
|
730
|
+
let items = laidOut.map(entry => ({ elementId: entry.elementId, rect: rectOf(desktopBlock(entry.node)) }));
|
|
731
|
+
for (const entry of laidOut) {
|
|
732
|
+
const active = items.find(item => item.elementId === entry.elementId);
|
|
733
|
+
if (active)
|
|
734
|
+
items = moveActive(items, active);
|
|
735
|
+
}
|
|
736
|
+
let moved = 0;
|
|
737
|
+
for (const [index, entry] of laidOut.entries()) {
|
|
738
|
+
const item = items[index];
|
|
739
|
+
const block = desktopBlock(entry.node);
|
|
740
|
+
const rect = rectOf(block);
|
|
741
|
+
if (!item || item.rect.y === rect.y)
|
|
742
|
+
continue;
|
|
743
|
+
moved++;
|
|
744
|
+
if (block) {
|
|
745
|
+
block.position = { x: rect.x, y: item.rect.y };
|
|
746
|
+
continue;
|
|
747
|
+
}
|
|
748
|
+
entry.node.blocks = [...asObjects(entry.node.blocks), {
|
|
749
|
+
breakpoint: DESKTOP_BREAKPOINT,
|
|
750
|
+
position: { x: rect.x, y: item.rect.y },
|
|
751
|
+
size: { w: rect.w, h: rect.h }
|
|
752
|
+
}];
|
|
753
|
+
}
|
|
754
|
+
return moved;
|
|
755
|
+
}
|
|
756
|
+
function reorderPass(nodes) {
|
|
757
|
+
const siblings = asObjects(nodes);
|
|
758
|
+
let moved = reorderSiblings(siblings);
|
|
759
|
+
for (const node of siblings) {
|
|
760
|
+
moved += reorderPass(node.children);
|
|
761
|
+
}
|
|
762
|
+
return moved;
|
|
763
|
+
}
|
|
764
|
+
function collectLayout(nodes, collected) {
|
|
765
|
+
for (const node of asObjects(nodes)) {
|
|
766
|
+
const block = desktopBlock(node);
|
|
767
|
+
collected.push({
|
|
768
|
+
node,
|
|
769
|
+
label: String(node.elementId ?? node.name ?? node.type ?? '?'),
|
|
770
|
+
y: rectOf(block).y,
|
|
771
|
+
hasBlock: block !== undefined
|
|
772
|
+
});
|
|
773
|
+
collectLayout(node.children, collected);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* Bir geçiş her zaman yetmez: doluluk matrisinde bir hücreyi son yazan komponent öncekini gizler
|
|
778
|
+
* (`createMatrixFromItemsIgnore`), o yüzden aynı hücrede üçten fazla komponent varken ilk geçiş
|
|
779
|
+
* çakışmanın bir kısmını görmez. Studio'da bu durum hiç oluşmaz — orada her seferinde TEK komponent
|
|
780
|
+
* bırakılır — komple şema yazarken oluşur; geçiş kendini yineleyerek sabit noktaya iner. Her geçiş
|
|
781
|
+
* yalnız aşağı ittiği için tur sayısı sonludur, üst sınır patolojik gövdeye karşı emniyet kemeri.
|
|
782
|
+
*/
|
|
783
|
+
const MAX_REORDER_PASSES = 8;
|
|
784
|
+
/**
|
|
785
|
+
* Studio'da komponent tuvale bırakıldığında ızgara çakışmasını studio-api'nin sayfa kurma servisi
|
|
786
|
+
* çözer: `SchemaHelper.mergeComponents` yeni komponenti kardeşlerine ekleyip `reorderComponents` →
|
|
787
|
+
* `draggableElement.moveItem` ile çakışan kardeşleri aşağı iter (NgApps.UI
|
|
788
|
+
* `apps/studio-api/src/domain/page/shared/page-schema.helper.ts:28`). Gateway o servisten geçmez —
|
|
789
|
+
* `page_create`/`page_update` şemayı POST/PUT ile KOMPLE yazar (`page_build` tool'u yok) — bu yüzden
|
|
790
|
+
* ajanın verdiği koordinatlar olduğu gibi kayda giriyor ve aynı hücreleri isteyen komponentler
|
|
791
|
+
* Studio tuvalinde üst üste biniyor. Burada aynı algoritma şemanın TAMAMINA uygulanır: en dıştaki
|
|
792
|
+
* kök dizi ve her `children` dizisi kendi ızgarası olarak ele alınır, o dizideki her komponent
|
|
793
|
+
* sırayla "bırakılan komponent" kabul edilir ve hücrelerini paylaşan kardeşleri altına iter. Dizinin
|
|
794
|
+
* kendi sırası ve komponentlerin x/genişlik/yükseklik değerleri değişmez, yalnız desktop block'unun
|
|
795
|
+
* `position.y` değeri kayar. Tam aynı hücreleri isteyen komponentlerden hangisinin üstte kalacağını
|
|
796
|
+
* Studio'nun bırakma mantığı belirler (sonra "bırakılan" kendi yerini alır), yani dizi sırası görsel
|
|
797
|
+
* sıraya birebir çevrilmez — garanti edilen şey çakışmasız ızgara. Çakışma yoksa hiçbir şeye
|
|
798
|
+
* dokunulmaz (idempotent). Kök seviyesindeki modal/drawer katmanları sayfa akışında durmadığı için
|
|
799
|
+
* dışarıda bırakılır (bkz. ROOT_LAYER_TYPES).
|
|
800
|
+
*/
|
|
801
|
+
export function reorderSchemaBlocks(nodes) {
|
|
802
|
+
const before = [];
|
|
803
|
+
collectLayout(nodes, before);
|
|
804
|
+
for (let pass = 0; pass < MAX_REORDER_PASSES && reorderPass(nodes) > 0; pass++)
|
|
805
|
+
continue;
|
|
806
|
+
return before
|
|
807
|
+
.filter(entry => rectOf(desktopBlock(entry.node)).y !== entry.y)
|
|
808
|
+
.map(entry => `${entry.label} y:${entry.y}→${rectOf(desktopBlock(entry.node)).y}${entry.hasBlock ? '' : " (desktop block'u yazıldı)"}`);
|
|
809
|
+
}
|
|
810
|
+
export function reorderNote(moved) {
|
|
811
|
+
if (moved.length === 0)
|
|
812
|
+
return [];
|
|
813
|
+
return [`çakışan komponentler ızgarada aşağı kaydırıldı (${moved.join(', ')}): Studio komponenti tuvale bırakırken bu itmeyi kendi yapar, şema komple yazıldığında o yol çalışmaz ve komponentler tuvalde üst üste binerdi`];
|
|
814
|
+
}
|
|
815
|
+
/**
|
|
816
|
+
* Studio HER kayıtta `page_container` konumunu (0,0)'a sıfırlar (NgApps.UI `page.mappers.ts`
|
|
817
|
+
* `mapPageSchemaRaw:44-49`); gateway o mapper'dan geçmediği için ajanın verdiği konum aynen kayda
|
|
818
|
+
* giriyordu. Layout'lu sayfada sayfa şeması slot'un İÇİNE enjekte edildiğinden (`mapPageLayoutToPreview`)
|
|
819
|
+
* y≠0 kapsayıcı, slot içinde aşağı kayar ve üstte y×10px boş bant çizilir — "üstte kocaman boşluk,
|
|
820
|
+
* içerik scroll'da" semptomunun S-1 üreticisi (21 §1). %100 türetilebilir olduğu için lint değil
|
|
821
|
+
* normalizasyon.
|
|
822
|
+
*/
|
|
823
|
+
export function resetPageContainerPosition(schema) {
|
|
824
|
+
const container = asObjects(schema).find(node => node.elementId === PAGE_CONTAINER_ELEMENT_ID);
|
|
825
|
+
if (!container)
|
|
826
|
+
return [];
|
|
827
|
+
const block = desktopBlock(container);
|
|
828
|
+
if (!block)
|
|
829
|
+
return [];
|
|
830
|
+
const rect = rectOf(block);
|
|
831
|
+
if (rect.x === 0 && rect.y === 0)
|
|
832
|
+
return [];
|
|
833
|
+
block.position = { x: 0, y: 0 };
|
|
834
|
+
return [`page_container konumu (${rect.x},${rect.y})→(0,0) sıfırlandı: Studio her kayıtta bunu kendisi yapar (page.mappers.ts mapPageSchemaRaw); sıfırlanmazsa sayfa layout slot'unun içinde aşağı kayar ve üstte boş bant çizilir`];
|
|
835
|
+
}
|
|
836
|
+
/**
|
|
837
|
+
* Patch hattı kapsayıcıyı DÜZELTEMEZ: `move_component` hedefi `page_container` olamaz (PAGE-006
|
|
838
|
+
* guard'ı) ve micro-op sözlüğünde blok konumu yazan başka op yok. Kayıtlı sayfada y≠0 kapsayıcı
|
|
839
|
+
* görülürse tek çıkış yolu `page_update(schema)` — normalizasyon orada çalışır; bu uyarı o yolu
|
|
840
|
+
* gösterir.
|
|
841
|
+
*/
|
|
842
|
+
export function pageContainerPositionWarning(schema) {
|
|
843
|
+
const container = asObjects(schema).find(node => node.elementId === PAGE_CONTAINER_ELEMENT_ID);
|
|
844
|
+
if (!container)
|
|
845
|
+
return [];
|
|
846
|
+
const rect = rectOf(desktopBlock(container));
|
|
847
|
+
if (rect.x === 0 && rect.y === 0)
|
|
848
|
+
return [];
|
|
849
|
+
return [`PAGE-010 (uyarı): kayıtlı page_container (${rect.x},${rect.y}) konumunda — Studio bunu (0,0)'da tutar; layout'lu sayfada üstte ~${rect.y * 10}px boş bant çizilir. Düzeltme: page_update'i schema parametresiyle çağırın (gateway konumu otomatik sıfırlar); move_component kapsayıcıya uygulanamaz (PAGE-006)`];
|
|
850
|
+
}
|
|
851
|
+
const LEADING_GAP_ROWS = 4;
|
|
852
|
+
const INTERNAL_GAP_ROWS = 12;
|
|
853
|
+
/**
|
|
854
|
+
* `layout: 'flex'` verilmiş kapsayıcı mı? Şemada prop değeri `{ type:'static', data:'flex' }`
|
|
855
|
+
* sarmalıdır (Studio'nun `screen-builder-flex.functions.ts` → `getNodeFlexProps()` fonksiyonu da
|
|
856
|
+
* `props.<ad>.data` açar); sarmalsız yazılmış gövdeler için ham değer de okunur.
|
|
857
|
+
* Kaynak: `packages/ng-apps-screen-builder/src/flex-layout.ts` → `isFlexLayout()`.
|
|
858
|
+
*/
|
|
859
|
+
export function isFlexContainer(node) {
|
|
860
|
+
return staticPropValue(node, 'layout') === 'flex';
|
|
861
|
+
}
|
|
862
|
+
function staticPropValue(node, name) {
|
|
863
|
+
const props = (node.props ?? {});
|
|
864
|
+
const value = props[name];
|
|
865
|
+
if (value !== null && typeof value === 'object')
|
|
866
|
+
return value.data;
|
|
867
|
+
return value;
|
|
868
|
+
}
|
|
869
|
+
function gapWalk(parentLabel, children, code, onlyParents, isRoot, warnings, parentIsFlex) {
|
|
870
|
+
const placed = children
|
|
871
|
+
.filter(child => !(isRoot && ROOT_LAYER_TYPES.has(String(child.type ?? ''))))
|
|
872
|
+
.map(child => ({ child, block: desktopBlock(child) }))
|
|
873
|
+
.filter(entry => entry.block !== undefined)
|
|
874
|
+
.map(entry => ({ label: String(entry.child.elementId ?? entry.child.name ?? entry.child.type ?? '?'), rect: rectOf(entry.block) }))
|
|
875
|
+
.sort((a, b) => a.rect.y - b.rect.y);
|
|
876
|
+
if (!parentIsFlex && (onlyParents === undefined || onlyParents.has(parentLabel)) && placed.length > 0) {
|
|
877
|
+
const first = placed[0];
|
|
878
|
+
if (first.rect.y > LEADING_GAP_ROWS) {
|
|
879
|
+
warnings.push(`${code}: '${parentLabel}' içinde içerik y=${first.rect.y}'te başlıyor ('${first.label}') — üstte ~${first.rect.y * 10}px boş bant çizilir. Sayfa şeması layout slot'unun İÇİNE yerleşir; layout header'ı için boşluk bırakmayın, y'leri 0'dan başlatın (kasıtlı boşluksa yok sayın)`);
|
|
880
|
+
}
|
|
881
|
+
let bottom = first.rect.y + first.rect.h;
|
|
882
|
+
let bottomLabel = first.label;
|
|
883
|
+
for (const entry of placed.slice(1)) {
|
|
884
|
+
const gap = entry.rect.y - bottom;
|
|
885
|
+
if (gap > INTERNAL_GAP_ROWS) {
|
|
886
|
+
warnings.push(`${code}: '${parentLabel}' içinde '${bottomLabel}' (y ${bottom}'te biter) ile '${entry.label}' (y ${entry.rect.y}) arasında ${gap} satır (~${gap * 10}px) boşluk var — kasıtlı değilse y'leri bitişik verin`);
|
|
887
|
+
}
|
|
888
|
+
if (entry.rect.y + entry.rect.h > bottom) {
|
|
889
|
+
bottom = entry.rect.y + entry.rect.h;
|
|
890
|
+
bottomLabel = entry.label;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
for (const child of children) {
|
|
895
|
+
gapWalk(String(child.elementId ?? child.type ?? '?'), asObjects(child.children), code, onlyParents, false, warnings, isFlexContainer(child));
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
/**
|
|
899
|
+
* Sayfa/layout düzeyi olay adları sabittir: `mount` ("When page/layout loaded") ve `destroy`
|
|
900
|
+
* ("When page/layout left") — NgApps.UI `EventActions.svelte:139-150`; destroy akışlarını runtime
|
|
901
|
+
* sayfadan ayrılmadan koşar (`Runtime.svelte initDestroyGuard`). Platform uydurma adı 200 ile
|
|
902
|
+
* kaydeder ama runtime eşleştiremez ve bağ sessizce ölü kalır (PAGE-008'in sayfa/layout ikizi).
|
|
903
|
+
*/
|
|
904
|
+
const SCREEN_EVENT_NAMES = new Set(['mount', 'destroy']);
|
|
905
|
+
export function invalidScreenEventNames(events, code) {
|
|
906
|
+
return asObjects(events)
|
|
907
|
+
.map(event => String(event.name ?? ''))
|
|
908
|
+
.filter(name => name !== '' && !SCREEN_EVENT_NAMES.has(name))
|
|
909
|
+
.map(name => `${code}: sayfa/layout düzeyinde '${name}' diye bir olay YOK — geçerli adlar: mount (yüklendiğinde) · destroy (ayrılırken; temizlik akışları için). Platform kaydı kabul eder ama bağ sessizce ölü kalır.`);
|
|
910
|
+
}
|
|
911
|
+
const GUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
912
|
+
/**
|
|
913
|
+
* Sayfadaki/layouttaki widget düğümünde `type`, widget KAYDININ ID'sidir — slug değil (NgApps.UI
|
|
914
|
+
* `page.mappers.ts:187-194`: widget çözümü `widget.id === component.type`). Slug yazılırsa kayıt
|
|
915
|
+
* 200 döner, render sessizce boş kalır. GUID olmayan type kesin yanlıştır → block.
|
|
916
|
+
*/
|
|
917
|
+
export function invalidWidgetTypeNodes(nodes, code) {
|
|
918
|
+
const found = [];
|
|
919
|
+
for (const node of asObjects(nodes)) {
|
|
920
|
+
if (String(node.kind ?? '') === 'widget' && !GUID_PATTERN.test(String(node.type ?? ''))) {
|
|
921
|
+
found.push(`${code}: '${node.elementId ?? '?'}' düğümü kind:'widget' ama type ('${node.type ?? ''}') bir widget ID'si değil. Widget sayfaya SLUG ile değil KAYIT ID'siyle konur: component_list'ten widget'ın id'sini alıp type alanına yazın (platform_guide('component') §Widget).`);
|
|
922
|
+
}
|
|
923
|
+
found.push(...invalidWidgetTypeNodes(node.children, code));
|
|
924
|
+
}
|
|
925
|
+
return found;
|
|
926
|
+
}
|
|
927
|
+
/**
|
|
928
|
+
* S-2/S-3 üreticileri (21 §1): kapsayıcı içinde büyük öncü boşluk ya da kardeşler arası büyük dikey
|
|
929
|
+
* bant. Studio'da kullanıcı boşluğu tuvalde görüp düzeltir; MCP hattında kimse görmez. Bilinçli
|
|
930
|
+
* boşluk meşru olduğu için block değil warn; eşikler 21 §4'te (öncü > 4 satır, ara > 12 satır).
|
|
931
|
+
* `onlyParents` verilirse boşluk analizi yalnız o kapsayıcılarda yapılır (patch hattı dokunulan
|
|
932
|
+
* kapsayıcılarla sınırlı kalır, mevcut boşluklar her patch'te yeniden raporlanmaz); ağacın kalanı
|
|
933
|
+
* yine dolaşılır. `layout: 'flex'` kapsayıcıların İÇİ analiz DIŞIDIR: orada y yalnız sıradır,
|
|
934
|
+
* boşluk çizilmez (bkz. `isFlexContainer`) — ağaç yine dolaşılır, torunlar analiz edilir.
|
|
935
|
+
*/
|
|
936
|
+
export function verticalGapWarnings(nodes, code, onlyParents) {
|
|
937
|
+
const warnings = [];
|
|
938
|
+
gapWalk('kök', asObjects(nodes), code, onlyParents, true, warnings, false);
|
|
939
|
+
return warnings;
|
|
940
|
+
}
|
|
941
|
+
/**
|
|
942
|
+
* View gövdesinde alan→tablo bağı **alias** üzerinden kurulur (`ViewSourceMapper.Map`:
|
|
943
|
+
* `tablesByAlias.GetValueOrDefault(input.ViewEntityAlias) ?? tables.FirstOrDefault()`) ama
|
|
944
|
+
* OKUMA tarafı (`ViewDto`/`view_get`) alanı `viewEntityId` ile döndürür. Okuduğu şekli geri yazan
|
|
945
|
+
* ajanın alias'ı boş kalır ve platform **sessizce bütün alanları ilk tabloya** düşürür: çoklu
|
|
946
|
+
* entity'li view tek entity'ye çöker, üstelik istek 200 döner (17/H2). Türetilebilir olan yarısını
|
|
947
|
+
* (id → alias) çeviriyoruz; çevrilemeyeni block ediyoruz, çünkü sonucu sessizce yanlış indeks olur.
|
|
948
|
+
*/
|
|
949
|
+
export function normalizeViewDefinition(definition) {
|
|
950
|
+
const view = (definition ?? {});
|
|
951
|
+
const entities = asObjects(view.entities);
|
|
952
|
+
const aliasByEntityId = new Map();
|
|
953
|
+
const aliases = new Set();
|
|
954
|
+
for (const entity of entities) {
|
|
955
|
+
const alias = typeof entity.alias === 'string' ? entity.alias : '';
|
|
956
|
+
if (alias === '')
|
|
957
|
+
continue;
|
|
958
|
+
aliases.add(alias.toLowerCase());
|
|
959
|
+
if (typeof entity.entityId === 'string')
|
|
960
|
+
aliasByEntityId.set(entity.entityId, alias);
|
|
961
|
+
}
|
|
962
|
+
const fixes = [];
|
|
963
|
+
const errors = [];
|
|
964
|
+
for (const field of asObjects(view.fields)) {
|
|
965
|
+
const alias = typeof field.viewEntityAlias === 'string' ? field.viewEntityAlias : '';
|
|
966
|
+
const entityId = typeof field.viewEntityId === 'string' ? field.viewEntityId : (typeof field.entityId === 'string' ? field.entityId : '');
|
|
967
|
+
const label = String(field.alias ?? field.fieldId ?? '?');
|
|
968
|
+
if (alias !== '') {
|
|
969
|
+
if (!aliases.has(alias.toLowerCase())) {
|
|
970
|
+
errors.push(`'${label}' alanının viewEntityAlias'ı ('${alias}') entities listesindeki hiçbir alias ile eşleşmiyor`);
|
|
971
|
+
}
|
|
972
|
+
continue;
|
|
973
|
+
}
|
|
974
|
+
const resolved = aliasByEntityId.get(entityId);
|
|
975
|
+
if (resolved) {
|
|
976
|
+
field.viewEntityAlias = resolved;
|
|
977
|
+
delete field.viewEntityId;
|
|
978
|
+
fixes.push(`${label} → '${resolved}'`);
|
|
979
|
+
continue;
|
|
980
|
+
}
|
|
981
|
+
if (entities.length > 1) {
|
|
982
|
+
errors.push(`'${label}' alanı hangi entity'ye ait belirsiz (viewEntityAlias yok${entityId === '' ? '' : `, viewEntityId '${entityId}' entities listesinde değil`})`);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
if (errors.length > 0) {
|
|
986
|
+
errors.push(`Alanlar tabloya ALIAS ile bağlanır: entities: [{ entityId, alias }], fields: [{ viewEntityAlias: '<o alias>', fieldId, alias, weight }]. Eşleşmeyen alias platformda hataya değil SESSİZCE ilk tabloya düşmeye yol açar (VIEW-001) — mevcut alias'lar: ${[...aliases].join(', ') || '(yok)'}`);
|
|
987
|
+
}
|
|
988
|
+
return { fixes, errors };
|
|
989
|
+
}
|
|
990
|
+
export function viewAliasNote(fixes) {
|
|
991
|
+
if (fixes.length === 0)
|
|
992
|
+
return [];
|
|
993
|
+
return [`view alanlarının entity bağı id'den alias'a çevrildi (${fixes.join(', ')}): yazma tarafı viewEntityAlias ister, okuma tarafı viewEntityId döndürür — alias boş kalsa platform tüm alanları ilk tabloya düşürürdü`];
|
|
994
|
+
}
|
|
995
|
+
/**
|
|
996
|
+
* Değişken seti tek bir kayıtta yaşar: `CreateAppVariableCommand`/`CreatePageVariableCommand`
|
|
997
|
+
* yalnız `Data` alanı taşır (Studio uygulama başına ve sayfa başına TEK satır tutar —
|
|
998
|
+
* `IsVariableExistForApplicationAsync` doluysa create update'e döner). Studio'nun gönderdiği şekil
|
|
999
|
+
* `data`'nın **JSON string** olmasıdır: "[{\"key\":\"MyTest\",\"value\":{\"isActive\":true}}]".
|
|
1000
|
+
* `Data` tipi `object` olduğu için platform her gövdeyi kabul eder; alan adı tutmayan bir gövde
|
|
1001
|
+
* (name/type/defaultValue gibi) `Data`'yı null bırakır ve istek 200 döner ama HİÇBİR ŞEY yazılmaz
|
|
1002
|
+
* (17/Ç1). Okuma tarafı da aynı string'i geri verir (`AppVariablesDto.Map` → GetRawText).
|
|
1003
|
+
*/
|
|
1004
|
+
export function parseVariableEntries(data) {
|
|
1005
|
+
const source = typeof data === 'string' ? safeParse(data) : data;
|
|
1006
|
+
return asObjects(source)
|
|
1007
|
+
.filter(entry => typeof entry.key === 'string' && entry.key !== '')
|
|
1008
|
+
.map(entry => ({ key: String(entry.key), value: entry.value }));
|
|
1009
|
+
}
|
|
1010
|
+
function safeParse(value) {
|
|
1011
|
+
try {
|
|
1012
|
+
return JSON.parse(value);
|
|
1013
|
+
}
|
|
1014
|
+
catch {
|
|
1015
|
+
return [];
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
/**
|
|
1019
|
+
* Uç, seti KOMPLE değiştirir (`OverrideData`) — tek değişken göndermek diğerlerini siler. Gateway
|
|
1020
|
+
* mevcut seti okuyup anahtar bazında birleştirir; `replace: true` verilirse bilinçli olarak ezer.
|
|
1021
|
+
*/
|
|
1022
|
+
export function mergeVariableEntries(existing, incoming, replace) {
|
|
1023
|
+
if (replace)
|
|
1024
|
+
return { entries: incoming, fixes: [] };
|
|
1025
|
+
const merged = [...existing];
|
|
1026
|
+
const kept = [];
|
|
1027
|
+
for (const entry of incoming) {
|
|
1028
|
+
const index = merged.findIndex(current => current.key === entry.key);
|
|
1029
|
+
if (index === -1) {
|
|
1030
|
+
merged.push(entry);
|
|
1031
|
+
continue;
|
|
1032
|
+
}
|
|
1033
|
+
merged[index] = entry;
|
|
1034
|
+
}
|
|
1035
|
+
for (const entry of existing) {
|
|
1036
|
+
if (!incoming.some(current => current.key === entry.key))
|
|
1037
|
+
kept.push(entry.key);
|
|
1038
|
+
}
|
|
1039
|
+
return {
|
|
1040
|
+
entries: merged,
|
|
1041
|
+
fixes: kept.length > 0
|
|
1042
|
+
? [`mevcut değişkenler korundu (${kept.join(', ')}): uç seti komple değiştirir, gateway anahtar bazında birleştirdi — kasten silmek için replace: true verin`]
|
|
1043
|
+
: []
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
export function blockSizeNote(clamped) {
|
|
1047
|
+
if (clamped.length === 0)
|
|
1048
|
+
return [];
|
|
1049
|
+
return [`komponent boyutları katalogdaki maxSize'a çekildi (${clamped.join(', ')}): katalog varsayılanı kendi sınırını aşabiliyor, aşan gövde doğrulayıcıdan "must be between" hatasıyla dönerdi`];
|
|
1050
|
+
}
|
|
1051
|
+
/**
|
|
1052
|
+
* Sayfada page_container varken köke eklenen komponent kapsayıcının DIŞINDA kalır; Studio onu
|
|
1053
|
+
* tuvalde göstermez, sayfa düzeyi olaylar da kapsayıcıya bağlı olduğu için akış dışında kalır.
|
|
1054
|
+
* parent verilmemiş ya da 'root' verilmiş add_component'leri kapsayıcıya yönlendiriyoruz.
|
|
1055
|
+
*/
|
|
1056
|
+
export function retargetRootAdds(ops, containerElementId) {
|
|
1057
|
+
const retargeted = [];
|
|
1058
|
+
for (const op of asObjects(ops)) {
|
|
1059
|
+
if (op.op !== 'add_component')
|
|
1060
|
+
continue;
|
|
1061
|
+
if (op.parent !== undefined && op.parent !== null && op.parent !== 'root')
|
|
1062
|
+
continue;
|
|
1063
|
+
const component = op.component;
|
|
1064
|
+
if (ROOT_LAYER_TYPES.has(String(component?.type ?? '')))
|
|
1065
|
+
continue;
|
|
1066
|
+
op.parent = containerElementId;
|
|
1067
|
+
retargeted.push(String(component?.elementId ?? '?'));
|
|
1068
|
+
}
|
|
1069
|
+
return retargeted;
|
|
1070
|
+
}
|
|
1071
|
+
export function rootAddsNote(retargeted, containerElementId) {
|
|
1072
|
+
if (retargeted.length === 0)
|
|
1073
|
+
return [];
|
|
1074
|
+
return [`köke eklenmek istenen komponentler '${containerElementId}' kapsayıcısına alındı (${retargeted.join(', ')}): sayfa komponentleri kapsayıcının içinde durur, kök seviyesi Studio tuvalinin dışıdır`];
|
|
1075
|
+
}
|
|
1076
|
+
export function componentDefaultsNote(fixed) {
|
|
1077
|
+
if (fixed.length === 0)
|
|
1078
|
+
return [];
|
|
1079
|
+
return [`komponent gövdelerine eksik zorunlu alanlar eklendi (${fixed.join(' · ')}): bunlar olmadan platform NullReferenceException verir ve page_schema_validate bunu yakalamaz`];
|
|
1080
|
+
}
|
|
1081
|
+
/**
|
|
1082
|
+
* Create yolu junction kaydını açar ama fiziksel tabloyu kurmaz (platform bug'ı P-01,
|
|
1083
|
+
* EntityService.CreateAsync:172-191); update yolu kurar. Bu yüzden m2m/multi-file/multi-enum
|
|
1084
|
+
* alanları create gövdesinden ayrılır, entity onlarsız kurulur, alanlar entity_update ile
|
|
1085
|
+
* eklenir (klinik K-005).
|
|
1086
|
+
*/
|
|
1087
|
+
export function splitJunctionFields(definition, isJunction) {
|
|
1088
|
+
const fields = asObjects(definition.fields);
|
|
1089
|
+
const deferred = fields.filter(isJunction);
|
|
1090
|
+
if (deferred.length === 0)
|
|
1091
|
+
return { base: definition, deferred: [] };
|
|
1092
|
+
return { base: { ...definition, fields: fields.filter(field => !isJunction(field)) }, deferred };
|
|
1093
|
+
}
|
|
1094
|
+
/**
|
|
1095
|
+
* `remove_component` şemadan bir dalı çıkarır ama etkisi dalla sınırlı DEĞİLDİR: sayfa
|
|
1096
|
+
* kaydedilirken artık hiçbir komponent olayının, prop'unun ya da sayfa olayının referans vermediği
|
|
1097
|
+
* akışlar platformca KALICI olarak silinir (`PageService.UpdateAsync` → `flowRepository
|
|
1098
|
+
* .DeleteRangeAsync`). Micro-op hattının "yalnız hedefe dokunur" garantisinin tek istisnası budur;
|
|
1099
|
+
* çoğu zaman istenen davranıştır, istenmediğinde ise uyarı tek erken sinyaldir.
|
|
1100
|
+
*/
|
|
1101
|
+
/**
|
|
1102
|
+
* Repeater içinde repeater arar. ÖLÇÜLDÜ (2026-08-19, anket-admin probe sayfası): iç repeater
|
|
1103
|
+
* OKUMADA çalışır (dış satır başına doğru sayıda iç satır çizilir) ama YAZMA ölüdür —
|
|
1104
|
+
* iç girdiyi değiştirince dış satırın alanı değişmez ve iç repeater'ın `_repeaterValues`'ü null
|
|
1105
|
+
* kalır. Sebep yapısal: şemada iç repeater TEK düğümdür, dış satır başına ayrı düğüm yoktur;
|
|
1106
|
+
* `_repeaterValues` anahtarı iç satır id'sine göredir ve dış satırlar arasında çakışır.
|
|
1107
|
+
* Üstelik tek bir düzenlemeden sonra DOM bozuluyor (ölçümde girdi sayısı 3 → 19) ve ekranda
|
|
1108
|
+
* sürekli "Expression error: Cannot read properties of null" toast'ı çıkıyor.
|
|
1109
|
+
* Doğru desen: iç listeyi tek bir `textarea`'da satır başına bir öğe olarak topla.
|
|
1110
|
+
*/
|
|
1111
|
+
export function nestedRepeaterNodes(nodes) {
|
|
1112
|
+
const found = [];
|
|
1113
|
+
const walk = (list, insideRepeater) => {
|
|
1114
|
+
for (const node of list) {
|
|
1115
|
+
const isRepeater = node.type === 'repeater';
|
|
1116
|
+
if (isRepeater && insideRepeater)
|
|
1117
|
+
found.push(String(node.elementId ?? node.name ?? '?'));
|
|
1118
|
+
walk(asObjects(node.children), insideRepeater || isRepeater);
|
|
1119
|
+
}
|
|
1120
|
+
};
|
|
1121
|
+
walk(asObjects(nodes), false);
|
|
1122
|
+
return found;
|
|
1123
|
+
}
|
|
1124
|
+
export function nestedRepeaterMessage(ids) {
|
|
1125
|
+
return `PAGE-013 — repeater içinde repeater (istek gönderilmedi): ${ids.join(' · ')}. `
|
|
1126
|
+
+ 'Ölçüldü (2026-08-19): iç repeater OKUMADA çalışır ama girilen değer dış satıra GERİ YAZILMAZ '
|
|
1127
|
+
+ '(_repeaterValues null kalır) ve tek düzenlemeden sonra DOM bozulur. Sebep yapısal: şemada iç '
|
|
1128
|
+
+ 'repeater tek düğümdür, dış satır başına ayrı düğüm yoktur. Satır içi listeyi tek bir textarea '
|
|
1129
|
+
+ "ile topla (satır başına bir öğe) ve kaydederken split('\n') ile böl — platform_guide('page') Kural 1g.";
|
|
1130
|
+
}
|
|
1131
|
+
/**
|
|
1132
|
+
* `style` prop'u düz CSS STRING verilmiş düğümler. Çalışır (emotion string'i CSS metni sayar) ama
|
|
1133
|
+
* `--ng-*` token ezilemez, iç seçici/pseudo-class yazılamaz ve kabın kendi düzeni bozulabilir —
|
|
1134
|
+
* ölçüm (anket-admin giriş sayfası, 2026-08-19): string içindeki `display:flex` page_container'ın
|
|
1135
|
+
* kolon düzenini ezdi, kart 66px genişlik × 733px yükseklik çizildi.
|
|
1136
|
+
*/
|
|
1137
|
+
export function stringStyleNodes(nodes) {
|
|
1138
|
+
const found = [];
|
|
1139
|
+
const walk = (list) => {
|
|
1140
|
+
for (const node of list) {
|
|
1141
|
+
const props = (node.props ?? {});
|
|
1142
|
+
const style = props.style;
|
|
1143
|
+
if (style && typeof style.data === 'string' && style.data.includes(':')) {
|
|
1144
|
+
found.push(String(node.elementId ?? node.name ?? '?'));
|
|
1145
|
+
}
|
|
1146
|
+
walk(asObjects(node.children));
|
|
1147
|
+
}
|
|
1148
|
+
};
|
|
1149
|
+
walk(asObjects(nodes));
|
|
1150
|
+
return found;
|
|
1151
|
+
}
|
|
1152
|
+
export function stringStyleWarning(ids) {
|
|
1153
|
+
if (ids.length === 0)
|
|
1154
|
+
return [];
|
|
1155
|
+
return [`PAGE-014 (uyarı): style prop'u düz CSS STRING verilmiş (${ids.join(', ')}). `
|
|
1156
|
+
+ 'Çalışır ama --ng-* tema değişkeni ezemez, iç seçici/pseudo-class yazamazsın ve kabın kendi '
|
|
1157
|
+
+ 'düzenini bozabilirsin (ölçüm: string içindeki display:flex page_container\'ı çökertti, kart '
|
|
1158
|
+
+ '66px kaldı). style NESNE olmalı: { "borderRadius": "12px", "&__body": { "padding": "20px" } } '
|
|
1159
|
+
+ "— platform_guide('page') Kural 1h. Düzen için style'a HİÇ dokunma: kapsayıcının kendi "
|
|
1160
|
+
+ "layout:'flex' prop grubu var (Kural 1f-2)."];
|
|
1161
|
+
}
|
|
1162
|
+
/**
|
|
1163
|
+
* `layout` 'flex' DEĞİLKEN yazılmış flex düzen prop'ları. Renderer bunları yalnız
|
|
1164
|
+
* `isFlexLayout(props)` doğruyken okur (`flex-layout.ts` → `getFlexChildrenStyle`), Studio paneli de
|
|
1165
|
+
* `layout != 'flex'` iken alanları gizler (`component.functions.ts` `_hide` kuralları): ızgara
|
|
1166
|
+
* kapsayıcıya `direction: 'column'` yazmak SESSİZCE hiçbir şey yapmaz — doğrulama duvarı da
|
|
1167
|
+
* susar, çünkü prop katalogda VARDIR (2026-08-25 ölçümü: `page_schema_validate` yedi prop'a da
|
|
1168
|
+
* `valid: true` döndü). Katalog varsayılanına eşit değerler elenir; onlar niyet taşımaz.
|
|
1169
|
+
*/
|
|
1170
|
+
const FLEX_LAYOUT_DEFAULTS = {
|
|
1171
|
+
direction: 'row',
|
|
1172
|
+
horizontalAlign: 'left',
|
|
1173
|
+
verticalAlign: 'top',
|
|
1174
|
+
size: 'shrink',
|
|
1175
|
+
gap: 'medium',
|
|
1176
|
+
wrap: false
|
|
1177
|
+
};
|
|
1178
|
+
/** Kapsayıcı düzenini kuran prop kümesi (`ContainerComponent` şeması): `layout` + ona bağlı altı ayar. */
|
|
1179
|
+
export const FLEX_LAYOUT_PROP_NAMES = new Set(['layout', ...Object.keys(FLEX_LAYOUT_DEFAULTS)]);
|
|
1180
|
+
export function inertFlexPropNodes(nodes) {
|
|
1181
|
+
const found = [];
|
|
1182
|
+
const walk = (list) => {
|
|
1183
|
+
for (const node of list) {
|
|
1184
|
+
if (!isFlexContainer(node)) {
|
|
1185
|
+
const inert = Object.keys(FLEX_LAYOUT_DEFAULTS).filter(name => {
|
|
1186
|
+
const value = staticPropValue(node, name);
|
|
1187
|
+
return value !== undefined && value !== null && value !== FLEX_LAYOUT_DEFAULTS[name];
|
|
1188
|
+
});
|
|
1189
|
+
if (inert.length > 0)
|
|
1190
|
+
found.push(`${String(node.elementId ?? node.name ?? node.type ?? '?')} (${inert.join(', ')})`);
|
|
1191
|
+
}
|
|
1192
|
+
walk(asObjects(node.children));
|
|
1193
|
+
}
|
|
1194
|
+
};
|
|
1195
|
+
walk(asObjects(nodes));
|
|
1196
|
+
return found;
|
|
1197
|
+
}
|
|
1198
|
+
export function inertFlexPropWarning(entries) {
|
|
1199
|
+
if (entries.length === 0)
|
|
1200
|
+
return [];
|
|
1201
|
+
return [`PAGE-015 (uyarı): flex düzen prop'u verilmiş ama kapsayıcının layout'u 'flex' değil (${entries.join(' · ')}). `
|
|
1202
|
+
+ "direction · horizontalAlign · verticalAlign · size · gap · wrap YALNIZ layout:'flex' iken okunur; ızgarada "
|
|
1203
|
+
+ "sessizce yok sayılır (istek reddedilmez, prop katalogda vardır). Akış düzeni istiyorsan aynı düğüme "
|
|
1204
|
+
+ "layout:'flex' de yaz, istemiyorsan bu prop'ları kaldır — platform_guide('page') Kural 1f-2."];
|
|
1205
|
+
}
|
|
1206
|
+
export function orphanedFlowWarnings(schema, pageEvents, ops) {
|
|
1207
|
+
const removals = asObjects(ops).filter(op => op.op === 'remove_component');
|
|
1208
|
+
if (removals.length === 0)
|
|
1209
|
+
return [];
|
|
1210
|
+
const nodes = asObjects(schema);
|
|
1211
|
+
const warnings = [];
|
|
1212
|
+
for (const removal of removals) {
|
|
1213
|
+
const target = String(removal.target ?? '');
|
|
1214
|
+
const doomed = findNodeByElementId(nodes, target);
|
|
1215
|
+
if (!doomed)
|
|
1216
|
+
continue;
|
|
1217
|
+
const losing = collectFlowRefs(doomed);
|
|
1218
|
+
if (losing.size === 0)
|
|
1219
|
+
continue;
|
|
1220
|
+
const surviving = new Set();
|
|
1221
|
+
for (const event of asObjects(pageEvents)) {
|
|
1222
|
+
if (typeof event.value === 'string')
|
|
1223
|
+
surviving.add(event.value);
|
|
1224
|
+
}
|
|
1225
|
+
collectFlowRefsExcept(nodes, target, surviving);
|
|
1226
|
+
const orphaned = [...losing].filter(flowId => !surviving.has(flowId));
|
|
1227
|
+
if (orphaned.length > 0) {
|
|
1228
|
+
warnings.push(`'${target}' silinince ona bağlı ${orphaned.length} akış sayfada referanssız kalıyor (${orphaned.join(', ')}) ve platform bunları KALICI olarak siler. Akışı korumak istiyorsan önce başka bir komponente bağla (set_event) ya da flow_get ile gövdesini yedekle.`);
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
return warnings;
|
|
1232
|
+
}
|
|
1233
|
+
function findNodeByElementId(nodes, elementId) {
|
|
1234
|
+
for (const node of nodes) {
|
|
1235
|
+
if (node.elementId === elementId)
|
|
1236
|
+
return node;
|
|
1237
|
+
const found = findNodeByElementId(asObjects(node.children), elementId);
|
|
1238
|
+
if (found)
|
|
1239
|
+
return found;
|
|
1240
|
+
}
|
|
1241
|
+
return undefined;
|
|
1242
|
+
}
|
|
1243
|
+
function collectFlowRefs(node) {
|
|
1244
|
+
const found = new Set();
|
|
1245
|
+
const visitProps = (value) => {
|
|
1246
|
+
if (Array.isArray(value)) {
|
|
1247
|
+
for (const entry of value)
|
|
1248
|
+
visitProps(entry);
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
if (!value || typeof value !== 'object')
|
|
1252
|
+
return;
|
|
1253
|
+
const record = value;
|
|
1254
|
+
if (record.type === 'flow' && typeof record.ref === 'string')
|
|
1255
|
+
found.add(record.ref);
|
|
1256
|
+
for (const entry of Object.values(record))
|
|
1257
|
+
visitProps(entry);
|
|
1258
|
+
};
|
|
1259
|
+
/**
|
|
1260
|
+
* Bir düğümün akış bağları iki yerdedir: `events[].value` ve `type:'flow'` prop'ları.
|
|
1261
|
+
* Alt ağacı düğüm düğüm gez — `props`'u derin taramak yetmez, çünkü çocuğun `events`
|
|
1262
|
+
* girdisi `{ name, value }` şeklindedir ve `type:'flow'` testine takılmaz. Bu eksik
|
|
1263
|
+
* yüzünden kapsayıcı silindiğinde İÇİNDEKİ butonun click akışı sessizce yok sayılıyordu
|
|
1264
|
+
* (ölçüm: anket-admin giriş sayfası, 2026-08-19 — akış uyarısız silindi).
|
|
1265
|
+
*/
|
|
1266
|
+
const visitNode = (current) => {
|
|
1267
|
+
for (const event of asObjects(current.events)) {
|
|
1268
|
+
if (typeof event.value === 'string')
|
|
1269
|
+
found.add(event.value);
|
|
1270
|
+
}
|
|
1271
|
+
visitProps(current.props);
|
|
1272
|
+
for (const child of asObjects(current.children))
|
|
1273
|
+
visitNode(child);
|
|
1274
|
+
};
|
|
1275
|
+
visitNode(node);
|
|
1276
|
+
return found;
|
|
1277
|
+
}
|
|
1278
|
+
function collectFlowRefsExcept(nodes, skipElementId, into) {
|
|
1279
|
+
for (const node of nodes) {
|
|
1280
|
+
if (node.elementId === skipElementId)
|
|
1281
|
+
continue;
|
|
1282
|
+
for (const flowId of collectFlowRefsShallow(node))
|
|
1283
|
+
into.add(flowId);
|
|
1284
|
+
collectFlowRefsExcept(asObjects(node.children), skipElementId, into);
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
function collectFlowRefsShallow(node) {
|
|
1288
|
+
const found = new Set();
|
|
1289
|
+
for (const event of asObjects(node.events)) {
|
|
1290
|
+
if (typeof event.value === 'string')
|
|
1291
|
+
found.add(event.value);
|
|
1292
|
+
}
|
|
1293
|
+
const visit = (value) => {
|
|
1294
|
+
if (Array.isArray(value)) {
|
|
1295
|
+
for (const entry of value)
|
|
1296
|
+
visit(entry);
|
|
1297
|
+
return;
|
|
1298
|
+
}
|
|
1299
|
+
if (!value || typeof value !== 'object')
|
|
1300
|
+
return;
|
|
1301
|
+
const record = value;
|
|
1302
|
+
if (record.type === 'flow' && typeof record.ref === 'string')
|
|
1303
|
+
found.add(record.ref);
|
|
1304
|
+
for (const entry of Object.values(record))
|
|
1305
|
+
visit(entry);
|
|
1306
|
+
};
|
|
1307
|
+
visit(node.props);
|
|
1308
|
+
return found;
|
|
1309
|
+
}
|