@appaflytech/zeyrek-ai-mcp 0.0.7 → 0.0.8
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/dist/knowledge/backendConstraints.js +3 -0
- package/dist/knowledge/componentCatalog.js +59 -15
- package/dist/knowledge/normalize.js +459 -23
- package/dist/knowledge/propTypes.js +5 -4
- package/dist/knowledge/rules.js +110 -5
- package/dist/knowledge/styleResolve.js +6 -1
- package/dist/platform/http.js +10 -2
- package/dist/platform/screenshot.js +80 -8
- package/dist/platform/strictArguments.js +32 -0
- package/dist/platform/writeDiff.js +6 -3
- package/dist/resources/examples/page-sign-in.json +1 -1
- package/dist/resources/guides/component.md +1 -1
- package/dist/resources/guides/expressions.md +6 -0
- package/dist/resources/guides/getting-started.md +2 -0
- package/dist/resources/guides/layout.md +6 -0
- package/dist/resources/guides/microservice.md +105 -5
- package/dist/resources/guides/page.md +81 -18
- package/dist/resources/guides/translation.md +11 -10
- package/dist/resources/guides/workflow.md +50 -1
- package/dist/resources/schema-fingerprint.json +2 -2
- package/dist/resources/schemas/CreateMicroserviceCommand.json +4 -1
- package/dist/resources/schemas/CreateWorkflowCommand.json +4 -1
- package/dist/resources/schemas/PreviewMicroserviceCommand.json +4 -1
- package/dist/resources/schemas/PreviewWorkflowCommand.json +4 -1
- package/dist/resources/schemas/StepDto.json +4 -1
- package/dist/resources/schemas/UpdateMicroserviceCommand.json +4 -1
- package/dist/resources/schemas/UpdateWorkflowCommand.json +4 -1
- package/dist/server.js +2 -1
- package/dist/tools/flows.js +9 -3
- package/dist/tools/layouts.js +4 -4
- package/dist/tools/microservices.js +70 -6
- package/dist/tools/pages.js +127 -57
- package/dist/tools/queries.js +72 -6
- package/dist/tools/workflows.js +9 -5
- package/package.json +1 -1
package/dist/knowledge/rules.js
CHANGED
|
@@ -125,7 +125,7 @@ const conditionRuleIdPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}
|
|
|
125
125
|
// Runtime.svelte'de HAM verilir (`argument: step.args`), yani bu alanlarda ifade değerlendirilmez.
|
|
126
126
|
const flowLiteralTextFields = new Map([
|
|
127
127
|
['alert', ['message', 'description']],
|
|
128
|
-
['confirm', ['message']]
|
|
128
|
+
['confirm', ['message', 'title', 'confirmText', 'cancelText']]
|
|
129
129
|
]);
|
|
130
130
|
const flowExpressionLooking = /\$\{|\b(?:Steps|Page|Row|Widget|LocalStorage|state|self|item|event|error|cookie)\s*(?:\?\.|\.|\[)/;
|
|
131
131
|
// Barındırıcıya özel tipler (`navigate` web'de, `confirm`/`download` mobilde register edilmez)
|
|
@@ -235,7 +235,7 @@ export function flowExpressionSites(step) {
|
|
|
235
235
|
}
|
|
236
236
|
return sites;
|
|
237
237
|
}
|
|
238
|
-
const writeOperations = new Set(['create', 'update', 'delete', 'upsert']);
|
|
238
|
+
const writeOperations = new Set(['create', 'update', 'delete', 'upsert', 'bulk-create', 'bulk-update', 'bulk-delete']);
|
|
239
239
|
/** Built-in `roles` entity'si ve slug alanı — SeedData her projeye SABİT id'lerle açar (EntityConstants.cs). */
|
|
240
240
|
const rolesEntityId = '01917ede-88a2-72ea-8397-2619e5d5dca0';
|
|
241
241
|
const rolesSlugFieldId = '0199c35d-cfc8-77d4-9c20-29b857153147';
|
|
@@ -346,6 +346,38 @@ function unreadSelectFields(step, steps) {
|
|
|
346
346
|
.map(field => String(field.fieldSlug ?? field.slug ?? ''))
|
|
347
347
|
.filter(slug => slug.length > 0 && !new RegExp(`\\b${escapeForPattern(slug)}\\b`).test(usage));
|
|
348
348
|
}
|
|
349
|
+
const SQL_COMMENT_PATTERN = /--[^\n]*|\/\*[\s\S]*?\*\//g;
|
|
350
|
+
const QUERY_PARAMETER_TOKEN = String.raw `(?:\{\{\s*([A-Za-z_]\w*)\s*\}\}|(?<![:@\w'])@([A-Za-z_]\w*))(?:\s*::\s*\w+)*`;
|
|
351
|
+
const EMPTY_STRING_COMPARISON_PATTERN = new RegExp(`${QUERY_PARAMETER_TOKEN}\\s*=\\s*(?<!')''(?!')|(?<!')''\\s*=\\s*${QUERY_PARAMETER_TOKEN}`, 'g');
|
|
352
|
+
/**
|
|
353
|
+
* `rawQuery`'de bir parametreyi boş string'le EŞİTLİK üzerinden karşılaştıran yerlerin parametre
|
|
354
|
+
* adları (`@p = ''`, `@p::text = ''`, `'' = {{p}}`). Yorumlar önce atılır — platform da yorum
|
|
355
|
+
* içindeki belirteci parametre saymıyor. Yalnız `=` aranır: `@p <> ''` boş değerde NULL (yanlış)
|
|
356
|
+
* verir ve "doluysa" niyetiyle yazıldığında sonuç zaten doğrudur.
|
|
357
|
+
*/
|
|
358
|
+
function emptyStringComparisons(rawQuery) {
|
|
359
|
+
if (typeof rawQuery !== 'string')
|
|
360
|
+
return [];
|
|
361
|
+
const names = new Set();
|
|
362
|
+
for (const match of rawQuery.replace(SQL_COMMENT_PATTERN, ' ').matchAll(EMPTY_STRING_COMPARISON_PATTERN)) {
|
|
363
|
+
const name = match[1] ?? match[2] ?? match[3] ?? match[4];
|
|
364
|
+
if (name !== undefined)
|
|
365
|
+
names.add(name);
|
|
366
|
+
}
|
|
367
|
+
return [...names];
|
|
368
|
+
}
|
|
369
|
+
/** `rawQuery`'nin başvurduğu parametre adları (`@p`, `{{p}}`), yorumlar atılarak; ilk görülme sırasıyla. */
|
|
370
|
+
export function rawQueryParameterNames(rawQuery) {
|
|
371
|
+
if (typeof rawQuery !== 'string')
|
|
372
|
+
return [];
|
|
373
|
+
const names = new Set();
|
|
374
|
+
for (const match of rawQuery.replace(SQL_COMMENT_PATTERN, ' ').matchAll(new RegExp(QUERY_PARAMETER_TOKEN, 'g'))) {
|
|
375
|
+
const name = match[1] ?? match[2];
|
|
376
|
+
if (name !== undefined)
|
|
377
|
+
names.add(name);
|
|
378
|
+
}
|
|
379
|
+
return [...names];
|
|
380
|
+
}
|
|
349
381
|
const isRecord = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
350
382
|
const functionsGuide = 'ngapps://guides/functions';
|
|
351
383
|
export const policyRules = [
|
|
@@ -418,7 +450,7 @@ export const policyRules = [
|
|
|
418
450
|
id: 'MS-016',
|
|
419
451
|
module: 'microservice',
|
|
420
452
|
appliesTo: ['create', 'update', 'preview'],
|
|
421
|
-
sourceRef: "İK koşusu M-3: operation 'upsert' olan uçta expression'la yazılan alan UPDATE yolunda yeniden değerlendiriliyor; Incremental sicil alanı EMP-0005 → EMP-0006 diye sessizce değişti.
|
|
453
|
+
sourceRef: "İK koşusu M-3: operation 'upsert' olan uçta expression'la yazılan alan UPDATE yolunda yeniden değerlendiriliyor; Incremental sicil alanı EMP-0005 → EMP-0006 diye sessizce değişti. Alan bazlı writeOn (create|update|always) 2026-09-04'te eklendi (B-12), bu yüzden kural artık çözümü söyler ve writeOn:'create' taşıyan alanda susar",
|
|
422
454
|
check: definition => {
|
|
423
455
|
const parameters = (definition.sourceParameters ?? {});
|
|
424
456
|
if (String(parameters.operation ?? '') !== 'upsert')
|
|
@@ -426,13 +458,14 @@ export const policyRules = [
|
|
|
426
458
|
const generated = asArray(parameters.fields)
|
|
427
459
|
.map(entry => entry)
|
|
428
460
|
.filter(field => String(field.source ?? '') === 'expression')
|
|
461
|
+
.filter(field => String(field.writeOn ?? 'always').toLowerCase() !== 'create')
|
|
429
462
|
.map(field => String(field.alias ?? field.fieldId ?? '?'));
|
|
430
463
|
if (generated.length === 0)
|
|
431
464
|
return [];
|
|
432
465
|
return [{
|
|
433
466
|
ruleId: 'MS-016',
|
|
434
467
|
severity: 'warn',
|
|
435
|
-
message: `upsert ucunda expression kaynaklı alan var (${generated.join(', ')}): upsert UPDATE yolunda da bu ifadeyi yeniden değerlendirir, yani sicil/kod gibi ÜRETİLEN bir değer güncellemede sessizce değişir.
|
|
468
|
+
message: `upsert ucunda expression kaynaklı alan var (${generated.join(', ')}): upsert UPDATE yolunda da bu ifadeyi yeniden değerlendirir, yani sicil/kod gibi ÜRETİLEN bir değer güncellemede sessizce değişir. Alan yalnız oluşturmada yazılacaksa fields[] öğesine writeOn: 'create' koyun — upsert'in UPDATE dalı o alanı satıra hiç koymaz ve bu uyarı susar. writeOn yalnız operation:'upsert' ucunda geçerlidir (MS-017) ve bir 'kısmi güncelleme' bayrağı DEĞİLDİR: listelenen alan gövdede gelmezse yine NULL yazılır.`,
|
|
436
469
|
guide: 'ngapps://guides/microservice'
|
|
437
470
|
}];
|
|
438
471
|
}
|
|
@@ -451,6 +484,23 @@ export const policyRules = [
|
|
|
451
484
|
guide: 'ngapps://guides/microservice'
|
|
452
485
|
}]
|
|
453
486
|
},
|
|
487
|
+
{
|
|
488
|
+
id: 'QRY-007',
|
|
489
|
+
module: 'query',
|
|
490
|
+
appliesTo: ['create', 'update', 'preview'],
|
|
491
|
+
sourceRef: "2026-09-15 bulgusu: pano sorgusu '(@tech = '' OR w.technician_id::text = @tech)' ile 0 satır döndü, hata yok. Kök neden Query.PrepareQueryParameters / NormalizePreviewParameters: boş ya da yalnız boşluktan oluşan değer NULL bağlanır, bu yüzden '@p = ''' hiçbir değerle doğru olmaz. Bulgunun 'dokunulmamış dropdown null gönderiyor' teşhisi yanlıştı — '' gönderen form da aynı sonucu alır. warn: kalıp hatalı ama yazmayı durdurmak gerekmiyor",
|
|
492
|
+
check: definition => {
|
|
493
|
+
const names = emptyStringComparisons(definition.rawQuery);
|
|
494
|
+
if (names.length === 0)
|
|
495
|
+
return [];
|
|
496
|
+
return [{
|
|
497
|
+
ruleId: 'QRY-007',
|
|
498
|
+
severity: 'warn',
|
|
499
|
+
message: `rawQuery parametreyi boş string'le karşılaştırıyor (${names.map(name => `@${name} = ''`).join(', ')}): platform boş ya da yalnız boşluktan oluşan parametre değerini NULL bağlar, NULL = '' hiçbir zaman doğru olmaz ve 'filtre boşsa hepsini getir' dalı ASLA çalışmaz — filtre boş geldiğinde sorgu hatasız 0 satır döner. Değerin form bileşeninden null ya da '' gelmesi fark etmez. Şöyle yazın: (@${names[0]} IS NULL OR kolon::text = @${names[0]}).`,
|
|
500
|
+
guide: 'ngapps://guides/microservice'
|
|
501
|
+
}];
|
|
502
|
+
}
|
|
503
|
+
},
|
|
454
504
|
{
|
|
455
505
|
id: 'MS-018',
|
|
456
506
|
module: 'microservice',
|
|
@@ -2215,7 +2265,7 @@ export const policyRules = [
|
|
|
2215
2265
|
id: 'FLOW-022',
|
|
2216
2266
|
module: 'flow',
|
|
2217
2267
|
appliesTo: ['create', 'update'],
|
|
2218
|
-
sourceRef: "alert.ts `const { message, alertType, description } = task.argument` → toast.error(message, { description }) / dialog.alert(message); confirm.ts `dialog.confirm(
|
|
2268
|
+
sourceRef: "alert.ts `const { message, alertType, description } = task.argument` → toast.error(message, { description }) / dialog.alert(message); confirm.ts `dialog.confirm(message, { title, confirmText, cancelText })` (boş çerçeve metni dialog.functions.ts resolveDialogFrame'de sabit metne düşer). Argüman Runtime.svelte'de HAM geçer (`argument: step.args`) — bu alanlarda hiçbir ifade değerlendirilmez, yazılan metin ekranda harfi harfine görünür (2026-09-06·2 koşusu S-13'ün ikinci turu: rehber hata mesajını göstermek için alert adımını öneriyordu)",
|
|
2219
2269
|
check: definition => flowAllSteps(definition)
|
|
2220
2270
|
.flatMap((step) => {
|
|
2221
2271
|
const type = String(step.type ?? '');
|
|
@@ -2821,3 +2871,58 @@ export async function microfunctionStepFindings(platform, project, environment,
|
|
|
2821
2871
|
}
|
|
2822
2872
|
return applyPolicyMode(platform.config, findings, policyOverride);
|
|
2823
2873
|
}
|
|
2874
|
+
const partialWriteOperations = new Set(['create', 'update', 'upsert']);
|
|
2875
|
+
const inputReferencePattern = /\binput\.([A-Za-z_$][\w$]*)/g;
|
|
2876
|
+
function inputReferencesOf(rule) {
|
|
2877
|
+
return asArray(rule.fields)
|
|
2878
|
+
.filter((field) => Boolean(field) && typeof field === 'object')
|
|
2879
|
+
.flatMap(field => [...String(field.value ?? '').matchAll(inputReferencePattern)].map(match => match[1]))
|
|
2880
|
+
.filter((name) => typeof name === 'string');
|
|
2881
|
+
}
|
|
2882
|
+
/**
|
|
2883
|
+
* WF-031 — yazma adımı, akışın giriş DTO'sunda OLMAYAN bir `input.x` alanını yazıyorsa uyarır.
|
|
2884
|
+
* Böyle bir ifade çalışma anında `undefined` döner, `Parse` onu `null` yapar ve platform kolonu
|
|
2885
|
+
* NULL yazar: satırdaki mevcut değer silinir, yanıt yine 200'dür (risk uygulaması Kanban taşıma
|
|
2886
|
+
* bulgusu, 2026-09-15 — `{ id, status }` gövdesi `title`/`owner_id`/`due_date`'i sildi).
|
|
2887
|
+
*
|
|
2888
|
+
* Kural yalnız DTO'da HİÇ olmayan alanı sayar: "DTO'da var ama çağıran göndermedi" vakası
|
|
2889
|
+
* tanımdan görülemez, onu rehber anlatır. DTO şeması okunamazsa (404/erişim) kural susar —
|
|
2890
|
+
* bir okuma hatası yazma çağrısını engellememelidir.
|
|
2891
|
+
*/
|
|
2892
|
+
export async function undefinedInputFieldFindings(platform, project, environment, definition, kind, policyOverride = false) {
|
|
2893
|
+
if (platform.config.policyMode === 'off')
|
|
2894
|
+
return [];
|
|
2895
|
+
const body = workflowBody(definition, kind);
|
|
2896
|
+
if (!hasText(body.parameterDtoId))
|
|
2897
|
+
return [];
|
|
2898
|
+
const candidates = workflowSteps(body)
|
|
2899
|
+
.filter(step => step.type === 'table' && step.rule)
|
|
2900
|
+
.filter(step => partialWriteOperations.has(String(step.rule.operation ?? '').toLowerCase()))
|
|
2901
|
+
.map(step => ({ step, references: [...new Set(inputReferencesOf(step.rule))] }))
|
|
2902
|
+
.filter(candidate => candidate.references.length > 0);
|
|
2903
|
+
if (candidates.length === 0)
|
|
2904
|
+
return [];
|
|
2905
|
+
let dtoFieldNames;
|
|
2906
|
+
try {
|
|
2907
|
+
const payload = await platform.tenant('GET', project, `/datatransferobjects/${String(body.parameterDtoId)}/scheme`, { environment });
|
|
2908
|
+
const items = asArray(payload?.data ?? payload).filter((item) => Boolean(item) && typeof item === 'object');
|
|
2909
|
+
dtoFieldNames = new Set(items.map(item => String(item.name ?? '')));
|
|
2910
|
+
}
|
|
2911
|
+
catch (error) {
|
|
2912
|
+
if (error instanceof PlatformApiError)
|
|
2913
|
+
return [];
|
|
2914
|
+
throw error;
|
|
2915
|
+
}
|
|
2916
|
+
const findings = candidates.flatMap(({ step, references }) => {
|
|
2917
|
+
const missing = references.filter(name => !dtoFieldNames.has(name));
|
|
2918
|
+
return missing.length > 0
|
|
2919
|
+
? [{
|
|
2920
|
+
ruleId: 'WF-031',
|
|
2921
|
+
severity: 'warn',
|
|
2922
|
+
message: `'${stepLabel(step)}' adımı ${missing.map(name => `input.${name}`).join(', ')} alanlarını yazıyor ama akışın giriş DTO'sunda bu alanlar YOK: ifade undefined döner, platform kolonu NULL yazar ve satırdaki mevcut değer SİLİNİR (yanıt yine 200). Yazma adımında SET listesi fields[]'tir, gövde değil — 'göndermediğim alan değişmez' (REST PATCH) sezgisi burada geçmez. İki doğru çıkış: (1) adımı EYLEM BAŞINA daraltın, fields[] yalnız bu eylemin gerçekten yazdığı alanları listelesin (kanban taşıma: yalnız status); (2) alanı DTO'ya ekleyin VE çağıranın her istekte gönderdiğinden emin olun — DTO'ya eklemek yalnız izin verir, göndermeyi garanti etmez. Alan kümesi ancak çağrı anında belliyse akışta oku-birleştir: önce table get, sonra "typeof input.x !== 'undefined' && input.x !== null ? input.x : read_row.data.x" (bedeli fazladan okuma ve son-yazan-kazanır).`,
|
|
2923
|
+
guide: 'ngapps://guides/workflow'
|
|
2924
|
+
}]
|
|
2925
|
+
: [];
|
|
2926
|
+
});
|
|
2927
|
+
return applyPolicyMode(platform.config, findings, policyOverride);
|
|
2928
|
+
}
|
|
@@ -419,6 +419,12 @@ export const TYPED_STYLE_COMPONENTS = {
|
|
|
419
419
|
// Taşma hedefi olmayan komponentte contentOverflow ölü kural üretirdi.
|
|
420
420
|
omit: { surfaceStyle: ['contentOverflow'] }
|
|
421
421
|
},
|
|
422
|
+
form: {
|
|
423
|
+
// Çerçeve <form> KÖKÜNDE: yüzey köke, taşma gövdeye (S-19, 2026-09-15).
|
|
424
|
+
// Genel düğüm yüzeyi yolu değeri kökün dışındaki kutuya yazıp ikinci çerçeve çiziyordu.
|
|
425
|
+
groups: ['surfaceStyle'],
|
|
426
|
+
ext: { overflowTarget: '&__body' }
|
|
427
|
+
},
|
|
422
428
|
icon: {
|
|
423
429
|
// Yalnız hover: surface çipin (chipShape/chipColor), typography da boyutun
|
|
424
430
|
// (size) ve rengin (color) çift kanalı olurdu — görsel kararlar semantik
|
|
@@ -439,7 +445,6 @@ export const TYPED_STYLE_COMPONENTS = {
|
|
|
439
445
|
export const NODE_SURFACE_TYPES = new Set([
|
|
440
446
|
'table',
|
|
441
447
|
'datatable',
|
|
442
|
-
'form',
|
|
443
448
|
'collapsible-container',
|
|
444
449
|
'drawer',
|
|
445
450
|
'modal',
|
package/dist/platform/http.js
CHANGED
|
@@ -42,8 +42,16 @@ function appendQuery(url, query) {
|
|
|
42
42
|
if (value === undefined || value === null || value === '')
|
|
43
43
|
continue;
|
|
44
44
|
if (Array.isArray(value)) {
|
|
45
|
-
|
|
46
|
-
url.searchParams.append(key,
|
|
45
|
+
if (value.some(entry => entry !== null && typeof entry === 'object')) {
|
|
46
|
+
url.searchParams.append(key, JSON.stringify(value));
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
for (const entry of value)
|
|
50
|
+
url.searchParams.append(key, String(entry));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
else if (typeof value === 'object') {
|
|
54
|
+
url.searchParams.append(key, JSON.stringify(value));
|
|
47
55
|
}
|
|
48
56
|
else {
|
|
49
57
|
url.searchParams.append(key, String(value));
|
|
@@ -33,11 +33,54 @@ export function actionScript(action) {
|
|
|
33
33
|
return { found: true, applied: true, requests: count };
|
|
34
34
|
})()`;
|
|
35
35
|
}
|
|
36
|
+
// G-09 — kanban kartı ve takvim etkinliği HTML5 DnD ile taşınır. Olaylar sentetik atılır ve
|
|
37
|
+
// TEK DataTransfer'i paylaşır; koordinat bırakma hedefinin merkezidir, çünkü takvimin saat
|
|
38
|
+
// kolonu bırakılan saati `clientY`'den hesaplar (slot hedefi o saatin ortasına düşer).
|
|
39
|
+
// Playwright `dragTo` gerçek fare sürer ama ScreenshotPage bu yeteneği taşımaz; betik yolu
|
|
40
|
+
// öteki eylemlerle aynı ölçüm döngüsünden (requests · flowRuns) geçer.
|
|
41
|
+
if (action.action === 'drag') {
|
|
42
|
+
const destination = JSON.stringify(`[data-element-id="${String(action.to ?? '')}"]`);
|
|
43
|
+
const destinationName = JSON.stringify(String(action.to ?? ''));
|
|
44
|
+
return `(function () {
|
|
45
|
+
var count = performance.getEntriesByType('resource').length;
|
|
46
|
+
var node = document.querySelector(${selector});
|
|
47
|
+
if (!node) return { found: false, applied: false, requests: count };
|
|
48
|
+
var drop = document.querySelector(${destination});
|
|
49
|
+
if (!drop) return { found: false, applied: false, note: "sürükleme kaynağı bulundu ama bırakma hedefi ('to') sayfada yok: " + ${destinationName} + " — kanban kolonu <elementId>__col-<value>, takvim günü <elementId>__day-<yyyy-MM-dd>, saat dilimi <elementId>__slot-<yyyy-MM-dd>T<HH>", requests: count };
|
|
50
|
+
if (node.getAttribute('draggable') !== 'true') return { found: true, applied: false, note: "kaynak sürüklenebilir değil (draggable != true) — kanban isDraggable ya da eventcalendar isEditable kapalı olabilir", requests: count };
|
|
51
|
+
var transfer = new DataTransfer();
|
|
52
|
+
var center = function (element) { var rect = element.getBoundingClientRect(); return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 }; };
|
|
53
|
+
var fire = function (element, type, at) { element.dispatchEvent(new DragEvent(type, { bubbles: true, cancelable: true, composed: true, dataTransfer: transfer, clientX: at.x, clientY: at.y })); };
|
|
54
|
+
var from = center(node);
|
|
55
|
+
var to = center(drop);
|
|
56
|
+
fire(node, 'dragstart', from);
|
|
57
|
+
fire(drop, 'dragenter', to);
|
|
58
|
+
fire(drop, 'dragover', to);
|
|
59
|
+
fire(drop, 'drop', to);
|
|
60
|
+
fire(node, 'dragend', to);
|
|
61
|
+
return { found: true, applied: true, requests: count };
|
|
62
|
+
})()`;
|
|
63
|
+
}
|
|
64
|
+
const target = String(action.target ?? '');
|
|
65
|
+
const root = rootElementId(target);
|
|
66
|
+
// G-14 — `applied` yalnız "DOM'a yazıldı" demekti: tarih seçici olayı dinlemiyordu, kutu dolu
|
|
67
|
+
// görünüyor, prop boş kalıyordu. Kökün prop parmak izi eylemden ÖNCE alınır; runActions
|
|
68
|
+
// yerleşmeden sonra yeniden okuyup `propWritten`'ı çıkarır. Çok alanlı bileşenin kökünde tek
|
|
69
|
+
// değer yoktur — ilk kutuya yazmak yarım değeri "uygulandı" diye raporluyordu, kök reddedilir.
|
|
36
70
|
return `(function () {
|
|
37
71
|
var count = performance.getEntriesByType('resource').length;
|
|
38
72
|
var node = document.querySelector(${selector});
|
|
39
73
|
if (!node) return { found: false, applied: false, requests: count };
|
|
40
74
|
var wanted = String(${value});
|
|
75
|
+
var propsBefore = ${runtimePropsScript(root)};
|
|
76
|
+
var finish = function (result) { if (propsBefore !== undefined) result.propsBefore = propsBefore; return result; };
|
|
77
|
+
|
|
78
|
+
if (${JSON.stringify(root === target)}) {
|
|
79
|
+
var parts = Array.prototype.slice.call(node.querySelectorAll(${JSON.stringify(`[data-element-id^="${root}${INTERNAL_ROLE_SEPARATOR}"]`)}))
|
|
80
|
+
.filter(function (part) { return part.querySelector('input, select, textarea'); })
|
|
81
|
+
.map(function (part) { return part.getAttribute('data-element-id'); });
|
|
82
|
+
if (parts.length > 1) return finish({ found: true, applied: false, note: "çok alanlı bileşen: kökte tek değer yok, değeri kutulardan birine yazın — " + parts.join(' · '), requests: count });
|
|
83
|
+
}
|
|
41
84
|
|
|
42
85
|
// Açılır kutu (dropdown / multi-select) NATIVE alan değildir: değeri yazmak
|
|
43
86
|
// hiçbir şey yapmaz, seçeneğe TIKLANMASI gerekir. Liste ancak kutu açıkken
|
|
@@ -55,29 +98,46 @@ export function actionScript(action) {
|
|
|
55
98
|
var match = options.filter(function (option) { return option.getAttribute('data-value') === wanted; })[0]
|
|
56
99
|
|| options.filter(function (option) { return (option.textContent || '').trim() === wanted; })[0];
|
|
57
100
|
if (!match) {
|
|
58
|
-
resolve({ found: true, applied: false, note: "açılır kutu açıldı ama bu değere karşılık gelen seçenek yok: " + wanted + " — options listesini page_runtime_get ile doğrulayın; seçenek sayısı: " + options.length, requests: count });
|
|
101
|
+
resolve(finish({ found: true, applied: false, note: "açılır kutu açıldı ama bu değere karşılık gelen seçenek yok: " + wanted + " — options listesini page_runtime_get ile doğrulayın; seçenek sayısı: " + options.length, requests: count }));
|
|
59
102
|
return;
|
|
60
103
|
}
|
|
61
104
|
(match.querySelector('button') || match).click();
|
|
62
|
-
resolve({ found: true, applied: true, requests: count });
|
|
105
|
+
resolve(finish({ found: true, applied: true, requests: count }));
|
|
63
106
|
}, 300);
|
|
64
107
|
});
|
|
65
108
|
}
|
|
66
109
|
|
|
67
110
|
var field = node.matches('input, select, textarea') ? node : node.querySelector('input, select, textarea');
|
|
68
|
-
if (!field) return { found: true, applied: false, note: 'düğümde input/select/textarea yok — bu komponent native alan kullanmıyor olabilir, click ile deneyin', requests: count };
|
|
111
|
+
if (!field) return finish({ found: true, applied: false, note: 'düğümde input/select/textarea yok — bu komponent native alan kullanmıyor olabilir, click ile deneyin', requests: count });
|
|
69
112
|
var proto = field instanceof HTMLSelectElement ? HTMLSelectElement.prototype
|
|
70
113
|
: (field instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype);
|
|
71
114
|
if (field instanceof HTMLInputElement && (field.type === 'checkbox' || field.type === 'radio')) {
|
|
72
115
|
var shouldCheck = wanted === 'true' || wanted === '1' || wanted === 'on';
|
|
73
116
|
if (field.checked !== shouldCheck) field.click();
|
|
74
|
-
return { found: true, applied: true, requests: count };
|
|
117
|
+
return finish({ found: true, applied: true, requests: count });
|
|
75
118
|
}
|
|
76
119
|
var setter = Object.getOwnPropertyDescriptor(proto, 'value').set;
|
|
77
120
|
setter.call(field, ${value});
|
|
78
121
|
field.dispatchEvent(new Event('input', { bubbles: true }));
|
|
79
122
|
field.dispatchEvent(new Event('change', { bubbles: true }));
|
|
80
|
-
return { found: true, applied: true, requests: count };
|
|
123
|
+
return finish({ found: true, applied: true, requests: count });
|
|
124
|
+
})()`;
|
|
125
|
+
}
|
|
126
|
+
/** `rf_range__start` → `rf_range`: iç rol hedefinin prop'ları kök düğümde yaşar. */
|
|
127
|
+
export function rootElementId(target) {
|
|
128
|
+
const separator = target.indexOf(INTERNAL_ROLE_SEPARATOR);
|
|
129
|
+
return separator > 0 ? target.slice(0, separator) : target;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Düğümün runtime prop'larının parmak izi (`@ng-apps/flow` → flow-runtime-props.ts,
|
|
133
|
+
* `window.__ngAppsReadProps`). `undefined` okuyucu yayınlanmıyor (eski runtime) ya da prop'lar
|
|
134
|
+
* serileşmiyor; `null` hiçbir makine düğümü tanımıyor; dize karşılaştırılabilir iz.
|
|
135
|
+
*/
|
|
136
|
+
export function runtimePropsScript(elementId) {
|
|
137
|
+
return `(function () { /* runtimePropsSnapshot */
|
|
138
|
+
var read = window.__ngAppsReadProps;
|
|
139
|
+
if (typeof read !== 'function') return undefined;
|
|
140
|
+
try { var props = read(${JSON.stringify(elementId)}); return props === null ? null : JSON.stringify(props); } catch (error) { return undefined; }
|
|
81
141
|
})()`;
|
|
82
142
|
}
|
|
83
143
|
/**
|
|
@@ -428,19 +488,31 @@ export class StudioScreenshotter {
|
|
|
428
488
|
: navigated
|
|
429
489
|
? activityAfter.completed
|
|
430
490
|
: activityBefore === null ? undefined : Math.max(activityAfter.completed - activityBefore.completed, 0);
|
|
491
|
+
// G-14 — DOM'a yazmak değerin prop'a ulaştığını kanıtlamaz. İz yalnız okuyucu yayınlanıyor ve
|
|
492
|
+
// makine düğümü tanıyorsa (dize) karşılaştırılır; aksi hâlde propWritten bilinmez kalır.
|
|
493
|
+
const propsAfter = typeof before.propsBefore === 'string' && before.applied === true && !navigated
|
|
494
|
+
? await page.evaluate(runtimePropsScript(rootElementId(String(action.target ?? '')))).catch(() => undefined)
|
|
495
|
+
: undefined;
|
|
496
|
+
const propWritten = typeof propsAfter === 'string' ? propsAfter !== before.propsBefore : undefined;
|
|
497
|
+
const applied = before.applied === true && propWritten !== false;
|
|
498
|
+
const note = propWritten === false
|
|
499
|
+
? `değer DOM'a yazıldı ama '${rootElementId(String(action.target ?? ''))}' düğümünün runtime prop'ları DEĞİŞMEDİ — bileşen olayı kendi change yoluna çevirmedi ya da değer zaten aynıydı. Çok alanlı bileşende <elementId>__<rol> kutusunu hedefleyin (component_get → internalRoles).`
|
|
500
|
+
: before.note;
|
|
431
501
|
outcomes.push({
|
|
432
502
|
index,
|
|
433
503
|
action: String(action.action),
|
|
434
504
|
...(action.target === undefined ? {} : { target: action.target }),
|
|
505
|
+
...(action.to === undefined ? {} : { to: action.to }),
|
|
435
506
|
found: before.found === true,
|
|
436
|
-
applied
|
|
437
|
-
...(
|
|
507
|
+
applied,
|
|
508
|
+
...(propWritten === undefined ? {} : { propWritten }),
|
|
509
|
+
...(note === undefined ? {} : { note }),
|
|
438
510
|
requests,
|
|
439
511
|
...(flowRuns === undefined ? {} : { flowRuns }),
|
|
440
512
|
pageState: readiness.state
|
|
441
513
|
});
|
|
442
514
|
}
|
|
443
|
-
logStep('tarayıcı ⤵', `${outcomes.length} etkileşim adımı koştu`, outcomes.map(outcome => `${outcome.index}. ${outcome.action}${outcome.target ? ` '${outcome.target}'` : ''} → ${outcome.found ? (outcome.applied ? 'uygulandı' : 'bulundu ama uygulanamadı') : 'hedef BULUNAMADI'} · ${outcome.requests} ağ isteği`));
|
|
515
|
+
logStep('tarayıcı ⤵', `${outcomes.length} etkileşim adımı koştu`, outcomes.map(outcome => `${outcome.index}. ${outcome.action}${outcome.target ? ` '${outcome.target}'` : ''}${outcome.to ? ` ⇒ '${outcome.to}'` : ''} → ${outcome.found ? (outcome.applied ? 'uygulandı' : 'bulundu ama uygulanamadı') : 'hedef BULUNAMADI'} · ${outcome.requests} ağ isteği`));
|
|
444
516
|
return outcomes;
|
|
445
517
|
}
|
|
446
518
|
/**
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
function isRawShape(value) {
|
|
3
|
+
if (!value || typeof value !== 'object' || '_def' in value)
|
|
4
|
+
return false;
|
|
5
|
+
return Object.values(value).every(entry => entry instanceof z.ZodType);
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* GW-005 — tanınmayan üst düzey argüman REDDEDİLİR.
|
|
9
|
+
*
|
|
10
|
+
* SDK ham şekli `z.object(shape)` ile sarar ve zod'un varsayılanı bilinmeyen anahtarı hata vermeden
|
|
11
|
+
* ATMAKTIR: şemada olmayan bir alan doğrulamadan geçer, handler'a hiç ulaşmaz ve çağrı başarıyla
|
|
12
|
+
* biter. 2026-09-15 G-13'te `microservice_execute`/`microservice_debug`/`query_preview`'a verilen
|
|
13
|
+
* `parameters` böyle kayboldu — dört farklı filtre aynı gövdeyi döndü ve ölçüm aracı çağıranı
|
|
14
|
+
* olmayan bir sorgu kusuruna yönlendirdi. Tek tek tool'lara alan eklemek sınıfı kapatmaz; sessizlik
|
|
15
|
+
* burada, SDK'ya şema verilmeden önce kaldırılır.
|
|
16
|
+
*/
|
|
17
|
+
export function rejectUnknownArguments(server) {
|
|
18
|
+
const registerTool = server.registerTool.bind(server);
|
|
19
|
+
server.registerTool = ((name, config, callback) => {
|
|
20
|
+
if (!isRawShape(config.inputSchema))
|
|
21
|
+
return registerTool(name, config, callback);
|
|
22
|
+
const shape = config.inputSchema;
|
|
23
|
+
const accepted = Object.keys(shape).join(', ');
|
|
24
|
+
const inputSchema = z.object(shape, {
|
|
25
|
+
errorMap: (issue, context) => issue.code === 'unrecognized_keys'
|
|
26
|
+
? { message: `${name} şu üst düzey argümanları tanımıyor: ${issue.keys.map(key => `'${key}'`).join(', ')}. Tanınmayan alan sessizce yok sayılmaz — çağrı hiç gönderilmedi. Bu tool'un okuduğu alanlar: ${accepted}.` }
|
|
27
|
+
: { message: context.defaultError }
|
|
28
|
+
}).strict();
|
|
29
|
+
return registerTool(name, { ...config, inputSchema }, callback);
|
|
30
|
+
});
|
|
31
|
+
return server;
|
|
32
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { PROP_VALUE_TYPES } from '../knowledge/normalize.js';
|
|
1
2
|
import { logStep } from '../observability/trace.js';
|
|
2
3
|
const maxDrops = 20;
|
|
3
4
|
const maxDepth = 12;
|
|
@@ -214,9 +215,11 @@ export function unwrapProps(value) {
|
|
|
214
215
|
const record = value;
|
|
215
216
|
const isProp = typeof record.type === 'string' && (Object.hasOwn(record, 'data') || Object.hasOwn(record, 'ref'));
|
|
216
217
|
if (isProp) {
|
|
217
|
-
// `
|
|
218
|
-
//
|
|
219
|
-
|
|
218
|
+
// Platform `static` dışındaki her tipte değeri `ref`'e yazar (Prop ctor'u,
|
|
219
|
+
// DesignComponentManagementSchema.cs:666); `data` ya boştur ya türetilmiştir (file URL'i, page
|
|
220
|
+
// slug'ı, translation tasarım yedeği). `data`'yı taşıyıcı saymak bağlı akışı her yazmada
|
|
221
|
+
// "kaydedilen null" gösteriyordu (S-7).
|
|
222
|
+
const carrier = record.type !== 'static' && PROP_VALUE_TYPES.has(record.type)
|
|
220
223
|
? (Object.hasOwn(record, 'ref') ? record.ref : record.data)
|
|
221
224
|
: (Object.hasOwn(record, 'data') ? record.data : record.ref);
|
|
222
225
|
return unwrapProps(carrier);
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"rawRedirect": "redirect isRaw:true iken path ÇOK SATIRLI JS bloğudur ve son ifadenin değeri hedef adrestir. 'Korunan sayfaya gitmek isterken login'e düşen kullanıcıyı geri gönderme' deseni budur; yoksa sabit bir sayfaya gidilir (pageId + path).",
|
|
14
14
|
"objectProp": "Prop değeri nesne olabilir: icon → { type:'static', ref:{ name:'progress_activity' } }.",
|
|
15
15
|
"pageIsPublic": "Giriş sayfasına middlewareIds BAĞLANMAZ (guard'lı olsaydı kullanıcı giriş yapamazdı). Buna karşılık diğer sayfalar ve korunan uçlar middlewareIds ile bağlanır.",
|
|
16
|
-
"notificationIsHidden": "notification'ın şemasında isHidden 2026-08-26'dan beri VARDIR: props.isHidden'ı doğrudan bildirime yazabilirsin. Bu örnekteki box_message sarmalayıcısı o dönemin kalıntısıdır (crm koşumu B-10) ve tek bir bildirim için ARTIK GEREKMEZ — burada duruyor çünkü şeridi bir kaba sarıp birden çok parçayı birlikte gösterip gizliyor. appearance
|
|
16
|
+
"notificationIsHidden": "notification'ın şemasında isHidden 2026-08-26'dan beri VARDIR: props.isHidden'ı doğrudan bildirime yazabilirsin. Bu örnekteki box_message sarmalayıcısı o dönemin kalıntısıdır (crm koşumu B-10) ve tek bir bildirim için ARTIK GEREKMEZ — burada duruyor çünkü şeridi bir kaba sarıp birden çok parçayı birlikte gösterip gizliyor. appearance'ta ise isHidden alanı YOKTUR (2026-09-15'te kaldırıldı) — görünürlük yalnız props.isHidden'dır.",
|
|
17
17
|
"validationShape": "Validation gövdesi DÖRT alan taşır: { type, message, value, isActive }. 'name' anahtarı JsonIgnore'dur — {name, value, message} yazarsan tip enum varsayılanına düşer ve alan sessizce REGEX doğrulamasına döner. Tip adları TİRELİDİR (max-length / min-length / max-value / min-value); 'maxLength' backend'e yazılır ama runtime doğrulayıcı onu tanımaz ve doğrulama hiç koşmaz. Gateway düzeltebildiğini düzeltir, kalanını PAGE-016 ile reddeder."
|
|
18
18
|
},
|
|
19
19
|
"pageCreateArgs": {
|
|
@@ -117,7 +117,7 @@ Her şema düğümünde iki sözlük vardır; ikisi de ortak render katmanında
|
|
|
117
117
|
| `widthType` | `cover` · `contain` · `auto` · `fixed` · `fill` | İlk ikisi görsel/medya sığdırma. Kalanı **flex (ve eski cssgrid) kabın çocuğunun genişlik modu** (2026-09-01): `auto` içerik kadar, `fixed` `widthString`'den, `fill` kabın tüm genişliği (satırda `flex:1 1 100%`, sütunda `align-self:stretch`). **Yazılmazsa eski 12 kolon `blocks.size.w` payı geçerlidir** — öncelik: `fill` > `fixed` > `auto` > span |
|
|
118
118
|
| `widthString` / `heightString` | CSS uzunluğu | Sabit ölçü; **yalnız ilgili `*Type: 'fixed'` iken okunur** (aksi PAGE-017 uyarısı). Kabul: çıplak sayı (px sayılır) · `px % vw vh vmin vmax rem em ch` birimli değer · `calc(...)`. Negatif/`pt`/`fr`/`auto` GEÇMEZ — platform kaydeder ama panel ve runtime sessizce düşürür (PAGE-018). Eski `widthPx` alanı 2026-09-01'de kaldırıldı |
|
|
119
119
|
| `marginType` / `marginValue` | `normal` · `none` · `custom` + px | Dış boşluk: **yazılmazsa `normal` sayılır** = her kenara 4px (`spacing.ts` `CANVAS_NORMAL_MARGIN`); custom=`marginValue`. Bileşenleri dip dibe dizmek için `none` ŞART — `gap:'none'` olsa bile normal margin komşular arasında 8px bırakır (sıkı dizilim tarifi: `platform_guide('page')` Kural 1f-2) |
|
|
120
|
-
| `paddingType` / `paddingValue` | `normal` · `none` · `custom` + px |
|
|
120
|
+
| `paddingType` / `paddingValue` | `normal` · `none` · `custom` + px | İç boşluk: normal=10px. **Yalnız droppable kaplarda ve tipli stil modelindeki yapraklarda (box · text · container · statcard · icon · form) çizilir**; `slot`, kabuk kökleri (`layout`/`layout-shell`) ve `modal`/`drawer`/`auth-layout` hiç almaz — platform 200 döner, ekran değişmez (PAGE-029). Değerler piksel SAYISIDIR: `24`, `"24px"` değil (PAGE-030) |
|
|
121
121
|
| `alignType` | `left` · `center` · `right` | İçerik hizalama (destekleyen komponentlerde) |
|
|
122
122
|
| `aspectRatio` | sayı | En-boy oranı kilidi (medya komponentleri) |
|
|
123
123
|
|
|
@@ -103,6 +103,7 @@ dalından devam eder.
|
|
|
103
103
|
| `event` | `{name, data, source: {id, props}}` |
|
|
104
104
|
| `cookie` | `cookie.get(name)` / `cookie.set(name, value, {expiresInHours, path})` / `cookie.remove(name)` — tarayıcı cookie'si |
|
|
105
105
|
| `setComponentProperty(name, key, value)` / `getComponentProperty(name, key)` | fonksiyon alternatifi |
|
|
106
|
+
| `resetForm(name)` | Formu sıfırlar: alanlar başlangıç değerine döner, form "gönderilmemiş" olur, doğrulama mesajları silinir. Kayıt sonrası "yeni kayıt" için alanları tek tek `null`'a çekme — gönderilmiş formda her biri "zorunlu alan" mesajı çıkarır. Ad ya da komponent değişkeni alır: `resetForm('risk_form')` / `resetForm(risk_form)`. **Yalnız UI flow kodunda** |
|
|
106
107
|
| `LocalStorage` · `Widget` · `params` | yerel veri · widget bağlamı · olay parametreleri |
|
|
107
108
|
| `t(key)` | Çeviri anahtarını çözer: aktif dil → projenin varsayılan dili → anahtarın kendisi. Boş string de "eksik" sayılır. Sözlük `machine.context.state.translations`'tan gelir; dil `language-selector` ile değişince bir sonraki çağrı yeni dili görür. **Yalnız UI flow kodunda vardır** — backend ifadelerinde karşılığı `Loc()`'tur (`platform_guide('workflow')`) | `t('home.welcome')` |
|
|
108
109
|
| `log(...)` | console (proxy'leri temizler) |
|
|
@@ -130,6 +131,11 @@ prop `type: 'code'` olur, **ifade `ref` alanına `{{ }}` içinde** yazılır, `d
|
|
|
130
131
|
değerini taşır:
|
|
131
132
|
`"text": { "type": "code", "data": "Button text", "ref": "{{ state.page.MyTest.isActive ? 'Test' : 'FalseTest' }}" }`
|
|
132
133
|
|
|
134
|
+
> **Prop'ta parantezsiz ifade METİNDİR (PAGE-032).** Akış adımındaki `value.type: 'code'`
|
|
135
|
+
> ifadeyi doğrudan koşturur; prop'taki `code` ise bir şablondur ve yalnız `{{ }}` içini
|
|
136
|
+
> çalıştırır. `"ref": "new Date().getFullYear()"` prop'a harfi harfine o metni yazar, hata
|
|
137
|
+
> üretmez — `page_schema_patch` bu yüzden parantezsiz prop ifadesini reddeder.
|
|
138
|
+
|
|
133
139
|
## 3. Adım argümanlarında `value.type` çözümlemesi
|
|
134
140
|
|
|
135
141
|
`microservice`/`setter` adımlarının `parameters[]`/`iteratees[]` değerlerinde:
|
|
@@ -141,6 +141,8 @@ koda baktığını gizlemek yerine raporlamak.
|
|
|
141
141
|
- microservice → `microservice_execute`, beklenmedik sonuçta `microservice_debug`
|
|
142
142
|
- workflow → `workflow_preview`; automation → `automation_trigger` + `automation_histories`
|
|
143
143
|
- "API 200 döndü" tek başına bitti saymaz; politika bulgularını (`policyFindings`) oku ve gereğini yap
|
|
144
|
+
- Tool'un şemasında olmayan üst düzey argüman **reddedilir** ve hata okunan alanları listeler
|
|
145
|
+
(GW-005). Bir argüman kabul edildiyse gateway onu okuyor demektir.
|
|
144
146
|
|
|
145
147
|
|
|
146
148
|
## Yazdıktan sonra `silentlyDropped` alanına bak
|
|
@@ -92,6 +92,12 @@ container isWrapper:true heightType:'fill' layout:'flex' direction:'row' gap:
|
|
|
92
92
|
uçludur (`y / auto`), yani sayfa uzadıkça slot uzar.
|
|
93
93
|
- **Kaydırma:** `fill` + `states.isScrollable:true` verilen düğüm büyümek yerine İÇİNDE
|
|
94
94
|
kaydırır — uzun içerikte kabuğu sabit tutmanın yolu budur.
|
|
95
|
+
- **Slot'un kenar boşluğu margin'dir, padding değil.** Slot droppable değildir; ona yazılan
|
|
96
|
+
`paddingType`/`paddingValue` hiçbir kutuya çizilmez (LAY-010). Her sayfanın kenardan uzak
|
|
97
|
+
durmasını tek yerden istiyorsan slot'a `appearance.marginType: 'custom'` + `marginValue: 24`
|
|
98
|
+
yaz — margin slot'un sarmalayıcısına iç boşluk olarak iner ve slot'un zemini olmadığı için
|
|
99
|
+
sonuç padding'le aynıdır. Studio iskeleti slot'a zaten 8px custom margin verir. Değer SAYIDIR
|
|
100
|
+
(`24`, `"24px"` değil — LAY-011). Her sayfaya sarmalayıcı container eklemek gerekmez.
|
|
95
101
|
- **MOBİL UYARISI:** bu ağaç `flex` olduğu için telefonda YIĞILMAZ (Kural 7) — 375px'te yan
|
|
96
102
|
menü ince bir şerite iner. Telefonda alt alta düşmesi gereken bir kabuk için kökü
|
|
97
103
|
`layout:'grid'` kur; o zaman `fill`'in flex anlamı (`flex:1 1 auto`) devreye girmez,
|