diff --git a/.claude/skills/form-compile/scripts/form-compile.ps1 b/.claude/skills/form-compile/scripts/form-compile.ps1 index cba3f986..aab26faf 100644 --- a/.claude/skills/form-compile/scripts/form-compile.ps1 +++ b/.claude/skills/form-compile/scripts/form-compile.ps1 @@ -1,4 +1,4 @@ -# form-compile v1.90 — Compile 1C managed form from JSON or object metadata +# form-compile v1.91 — Compile 1C managed form from JSON or object metadata # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [string]$JsonPath, @@ -4555,7 +4555,9 @@ function Emit-Attributes { if ($attr.main -eq $true -and $attr.type) { $mainSaved = ("$($attr.type)") -match '^(CatalogObject|DocumentObject|ChartOfAccountsObject|ChartOfCalculationTypesObject|ChartOfCharacteristicTypesObject|ExchangePlanObject|BusinessProcessObject|TaskObject)\.' -or ("$($attr.type)") -match 'RecordManager\.' } - if ($attr.savedData -eq $true -or $mainSaved) { + # Явный ключ savedData побеждает (в т.ч. false → суппресс авто-вывода $mainSaved); нет ключа → авто. + $emitSaved = if ($null -ne $attr.PSObject.Properties['savedData']) { $attr.savedData -eq $true } else { $mainSaved } + if ($emitSaved) { X "$innertrue" } # Save: сохранение значения реквизита в пользовательских настройках. true → имя; @@ -4655,7 +4657,9 @@ function Emit-Attributes { $st = $attr.settings X "$inner" $si = "$inner`t" - # Порядок платформы: ManualQuery, DynamicDataRead, QueryText, Field*, MainTable, ListSettings + # Порядок платформы: AutoFillAvailableFields, ManualQuery, DynamicDataRead, QueryText, Field*, MainTable, ListSettings + # AutoFillAvailableFields — дефолт true; эмитим только при заданном ключе (отклонение). + if ($null -ne $st.autoFillAvailableFields) { X "$si$(if ($st.autoFillAvailableFields){'true'}else{'false'})" } $hasQuery = $st.query -and "$($st.query)".Trim() $mq = if ($hasQuery -or $st.manualQuery -eq $true) { "true" } else { "false" } X "$si$mq" @@ -4830,6 +4834,8 @@ function Emit-Properties { } # Convert boolean to lowercase string (PS renders as True/False) $val = $p.Value + # Пустая строка = суппресс-маркер (напр. autoTitle:"" — не эмитить и не додумывать) + if ($val -is [string] -and $val -eq '') { continue } if ($val -is [bool]) { $val = if ($val) { "true" } else { "false" } } diff --git a/.claude/skills/form-compile/scripts/form-compile.py b/.claude/skills/form-compile/scripts/form-compile.py index ee50a010..e9c81e0d 100644 --- a/.claude/skills/form-compile/scripts/form-compile.py +++ b/.claude/skills/form-compile/scripts/form-compile.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# form-compile v1.90 — Compile 1C managed form from JSON or object metadata +# form-compile v1.91 — Compile 1C managed form from JSON or object metadata # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import copy @@ -4273,7 +4273,9 @@ def emit_attributes(lines, attrs, indent): if attr.get('main') is True and attr.get('type'): t = str(attr['type']) main_saved = bool(re.match(r'^(CatalogObject|DocumentObject|ChartOfAccountsObject|ChartOfCalculationTypesObject|ChartOfCharacteristicTypesObject|ExchangePlanObject|BusinessProcessObject|TaskObject)\.', t)) or ('RecordManager.' in t) - if attr.get('savedData') is True or main_saved: + # Явный ключ savedData побеждает (в т.ч. False → суппресс авто-вывода main_saved); нет ключа → авто. + emit_saved = (attr['savedData'] is True) if 'savedData' in attr else main_saved + if emit_saved: lines.append(f'{inner}true') # Save: сохранение значения реквизита в пользовательских настройках. true → имя; # строка/массив → под-поля с авто-префиксом "имя." (путь с точкой / UUID / =имя — как есть). @@ -4361,6 +4363,10 @@ def emit_attributes(lines, attrs, indent): s = attr['settings'] lines.append(f'{inner}') si = f'{inner}\t' + # Порядок платформы: AutoFillAvailableFields, ManualQuery, DynamicDataRead, QueryText, Field*, MainTable, ListSettings + # AutoFillAvailableFields — дефолт true; эмитим только при заданном ключе (отклонение). + if s.get('autoFillAvailableFields') is not None: + lines.append(f'{si}{"true" if s["autoFillAvailableFields"] else "false"}') # Порядок платформы: ManualQuery, DynamicDataRead, QueryText, Field*, MainTable, ListSettings has_query = bool(s.get('query') and str(s['query']).strip()) mq = 'true' if (has_query or s.get('manualQuery')) else 'false' @@ -4519,6 +4525,9 @@ def emit_properties(lines, props, indent): # Auto PascalCase xml_name = p_name[0].upper() + p_name[1:] + # Пустая строка = суппресс-маркер (напр. autoTitle:"" — не эмитить и не додумывать) + if isinstance(p_value, str) and p_value == '': + continue # Convert boolean to lowercase if isinstance(p_value, bool): val = 'true' if p_value else 'false' diff --git a/.claude/skills/form-decompile/scripts/form-decompile.ps1 b/.claude/skills/form-decompile/scripts/form-decompile.ps1 index 57a0441a..00d47531 100644 --- a/.claude/skills/form-decompile/scripts/form-decompile.ps1 +++ b/.claude/skills/form-decompile/scripts/form-decompile.ps1 @@ -1,4 +1,4 @@ -# form-decompile v0.66 — Decompile 1C managed Form.xml to JSON DSL (draft) +# form-decompile v0.68 — Decompile 1C managed Form.xml to JSON DSL (draft) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # ВНИМАНИЕ: раундтрип не гарантируется. Навык исключён из авто-использования моделью. param( @@ -1658,7 +1658,7 @@ $titleNode = $root.SelectSingleNode("lf:Title", $ns) if ($titleNode) { $t = Get-LangText $titleNode; if ($null -ne $t) { $dsl['title'] = $t } } # properties (прямые скаляры под
, PascalCase → camelCase) -$KNOWN_FORM_PROPS = @('AutoTitle','WindowOpeningMode','CommandBarLocation','SaveDataInSettings','AutoSaveDataInSettings','AutoTime','UsePostingMode','RepostOnWrite','AutoURL','AutoFillCheck','Customizable','EnterKeyBehavior','VerticalScroll','Width','Height','Group','UseForFoldersAndItems') +$KNOWN_FORM_PROPS = @('AutoTitle','WindowOpeningMode','CommandBarLocation','SaveDataInSettings','AutoSaveDataInSettings','AutoTime','UsePostingMode','RepostOnWrite','AutoURL','AutoFillCheck','Customizable','EnterKeyBehavior','VerticalScroll','Width','Height','Group','UseForFoldersAndItems','SaveWindowSettings') $props = [ordered]@{} foreach ($pn in $KNOWN_FORM_PROPS) { $v = Get-Child $root $pn @@ -1670,8 +1670,13 @@ foreach ($pn in $KNOWN_FORM_PROPS) { else { $props[$camel] = $v } } } -# autoTitle=false при наличии title — это инъекция компилятора, опускаем (валидируем раундтрипом) -if ($dsl.Contains('title') -and $props.Contains('autoTitle') -and $props['autoTitle'] -eq $false) { $props.Remove('autoTitle') } +# AutoTitle при наличии title: компилятор инъектит false (~95% форм). Зеркалим: +# - оригинал имеет AutoTitle=false → опускаем ключ (компилятор реинъектит при наличии title); +# - оригинал НЕ имеет AutoTitle (редкие 5%, напр. вспом. формы) → суппресс-маркер "" (не инъектить). +if ($dsl.Contains('title')) { + if (-not $props.Contains('autoTitle')) { $props['autoTitle'] = '' } + elseif ($props['autoTitle'] -eq $false) { $props.Remove('autoTitle') } +} if ($props.Count -gt 0) { $dsl['properties'] = $props } # MobileDeviceCommandBarContent (form-level) → список имён командных панелей/кнопок @@ -1746,10 +1751,27 @@ if ($elements) { foreach ($e in $elements) { [void]$elemList.Add($e) } } if ($elemList.Count -gt 0) { $dsl['elements'] = @($elemList) } # attributes +# Объектный тип (зеркало Test-IsObjectLikeType компилятора) — кандидат на авто-main эвристики 11b.3. +function Test-IsObjectLikeTypeDec([string]$type) { + if ([string]::IsNullOrEmpty($type)) { return $false } + if ($type -eq 'DynamicList' -or $type -eq 'ConstantsSet') { return $true } + return ($type -match '^(CatalogObject|DocumentObject|DataProcessorObject|ReportObject|ExternalDataProcessorObject|ExternalReportObject|BusinessProcessObject|TaskObject|ChartOfAccountsObject|ChartOfCharacteristicTypesObject|ChartOfCalculationTypesObject|ExchangePlanObject|InformationRegisterRecordSet|AccumulationRegisterRecordSet|AccountingRegisterRecordSet|CalculationRegisterRecordSet|InformationRegisterRecordManager)\.') +} $attrsNode = $root.SelectSingleNode("lf:Attributes", $ns) if ($attrsNode) { $attrs = New-Object System.Collections.ArrayList - foreach ($a in @($attrsNode.SelectNodes("lf:Attribute", $ns))) { + # Подавление авто-main (эвристика компилятора 11b.3): если НЕТ ни одного И ровно + # один реквизит объектного типа — компилятор пометит его main. В оригинале он НЕ main (раз тега нет) + # → ставим суппресс-маркер main:false. На формах с >1 объектным реквизитом / с явным main — не нужно. + $allAttrNodes = @($attrsNode.SelectNodes("lf:Attribute", $ns)) + $anyMainAttr = $false; $objLikeNodes = @() + foreach ($an in $allAttrNodes) { + if ((Get-Child $an 'MainAttribute') -eq 'true') { $anyMainAttr = $true } + $atype = Decompile-Type ($an.SelectSingleNode("lf:Type", $ns)) + if (Test-IsObjectLikeTypeDec "$atype") { $objLikeNodes += $an } + } + $suppressMainName = if ((-not $anyMainAttr) -and $objLikeNodes.Count -eq 1) { $objLikeNodes[0].GetAttribute("name") } else { $null } + foreach ($a in $allAttrNodes) { $ao = [ordered]@{} $ao['name'] = $a.GetAttribute("name") $ty = Decompile-Type ($a.SelectSingleNode("lf:Type", $ns)); if ($ty) { $ao['type'] = $ty } @@ -1761,12 +1783,13 @@ if ($attrsNode) { $ao['valueType'] = if ($vt) { $vt } else { '' } # пустой Settings → маркер "" } if ((Get-Child $a 'MainAttribute') -eq 'true') { $ao['main'] = $true } + elseif ($suppressMainName -and $ao['name'] -eq $suppressMainName) { $ao['main'] = $false } $vw = Decompile-XrFlag $a 'View'; if ($null -ne $vw) { $ao['view'] = $vw } $ed = Decompile-XrFlag $a 'Edit'; if ($null -ne $ed) { $ao['edit'] = $ed } # Title атрибута. Компилятор для не-main атрибута без ключа title додумывает заголовок # из имени. Поэтому: нет → суппресс-маркер ''; ru-only == авто-вывод → опускаем # ключ (компилятор воспроизведёт); иначе → явный заголовок. - $isMain = $ao.Contains('main') + $isMain = ($ao['main'] -eq $true) # именно true; main:false (суппресс-маркер) → не-main для Title $tNode = $a.SelectSingleNode("lf:Title", $ns) if ($tNode) { $t = Get-LangText $tNode @@ -1776,7 +1799,11 @@ if ($attrsNode) { } elseif (-not $isMain) { $ao['title'] = '' } + # SavedData: компилятор додумывает true для main-реквизита объектного типа (эвристика $mainSaved: + # Catalog/Document/ChartOf*/ExchangePlan/BusinessProcess/Task Object + RecordManager). Если оригинал + # тега не имеет — ставим суппресс-маркер savedData:false (как с MainAttribute). if ((Get-Child $a 'SavedData') -eq 'true') { $ao['savedData'] = $true } + elseif ($ao['main'] -eq $true -and "$($ao['type'])" -match '^(CatalogObject|DocumentObject|ChartOfAccountsObject|ChartOfCalculationTypesObject|ChartOfCharacteristicTypesObject|ExchangePlanObject|BusinessProcessObject|TaskObject)\.|RecordManager\.') { $ao['savedData'] = $false } # Save: сохранение значения реквизита в пользовательских настройках. Один Field=имя → save:true; # иначе снимаем префикс "имя." (голое имя/UUID/прочее — как есть) → строка (1) или массив. $saveNode = $a.SelectSingleNode("lf:Save", $ns) @@ -1848,6 +1875,8 @@ if ($attrsNode) { $setNode = $a.SelectSingleNode("lf:Settings", $ns) if ($setNode) { $so = [ordered]@{} + # AutoFillAvailableFields — дефолт true, платформа эмитит только отклонение (false). Захват «как есть». + $afaf = Get-Child $setNode 'AutoFillAvailableFields'; if ($null -ne $afaf) { $so['autoFillAvailableFields'] = ($afaf -eq 'true') } $mt = Get-Child $setNode 'MainTable'; if ($mt) { $so['mainTable'] = $mt } $qtNode = $setNode.SelectSingleNode("lf:QueryText", $ns) if ($qtNode -and $qtNode.InnerText) { $so['query'] = Maybe-ExternalizeQuery -queryText $qtNode.InnerText -listName "$($ao['name'])" } diff --git a/docs/form-dsl-spec.md b/docs/form-dsl-spec.md index b4022d93..bbaae1c9 100644 --- a/docs/form-dsl-spec.md +++ b/docs/form-dsl-spec.md @@ -49,7 +49,8 @@ | DSL ключ | XML элемент | Значения | |----------|-------------|----------| -| `autoTitle` | `<AutoTitle>` | `true` / `false` | +| `autoTitle` | `<AutoTitle>` | `true` / `false`. **При наличии `title` компилятор сам инъектит `false`** (≈95% форм). Маркер `""` подавляет инъекцию (редкие формы с title, но без `<AutoTitle>`) | +| `saveWindowSettings` | `<SaveWindowSettings>` | `true` / `false` | | `windowOpeningMode` | `<WindowOpeningMode>` | `LockOwnerWindow`, `Modeless` | | `commandBarLocation` | `<CommandBarLocation>` | `Top`, `Bottom`, `None` | | `saveDataInSettings` | `<SaveDataInSettings>` | `UseList`, `Use`, `DontUse` | @@ -768,14 +769,14 @@ Pages поддерживает `pagesRepresentation`: `None`, `TabsOnTop`, `Tabs |----------|-----|----------| | `name` | string | Имя реквизита (обязательно) | | `type` | string | Тип (shorthand) | -| `main` | bool | Основной реквизит формы | +| `main` | bool | Основной реквизит формы (`<MainAttribute>`). **`true`** → пометить главным. **`false`** → суппресс: подавить авто-вывод компилятора (эвристика «нет явного main + ровно 1 реквизит объектного типа → пометить его»). Нет ключа → авто-вывод | | `title` | string/object | Заголовок. **Нет ключа** → авто-вывод из имени (как у элементов; кроме `main`). **`""`** → подавить (`<Title>` не эмитится — так платформа и хранит реквизит без синонима). Строка → ru; объект `{ru,en}` → мультиязычный. Декомпилятор опускает ключ, когда ru-заголовок совпадает с авто-выводом из имени | | `view` | bool/object | Просмотр по ролям (`<View>`). См. §4.1c | | `edit` | bool/object | Редактирование по ролям (`<Edit>`). См. §4.1c | | `functionalOptions` | array | Функциональные опции (`<FunctionalOptions><Item>FunctionalOption.X</Item>…`). Массив имён; forgiving: `"X"`/`"FunctionalOption.X"`. Также у колонок (`columns[*]`) и команд (§7) | | `useAlways` | array | Поля, всегда читаемые (`<UseAlways><Field>Имя.Поле</Field>…`). Массив коротких имён полей (forgiving: с/без префикса `Имя.`). **Маркер `~`** (query-поля дин-списка): `~Остановлен` → `<Field>~Список.Остановлен</Field>` (префикс ставится ПОСЛЕ `~`; полная форма `~Список.Остановлен` тоже принимается verbatim). **Две формы**: этот массив на реквизите ИЛИ `useAlways: true` на колонке (`columns[*]`) — компилятор сливает. Для дин-списка — только массив (колонки не эмитятся, но формируют `<UseAlways>`) | | `valueType` | string | Тип значений у реквизита типа `ValueList` (`<Settings xsi:type="v8:TypeDescription">`). Грамматика — как у `type`, включая составной `A \| B`. **Три состояния**: нет ключа → нет `<Settings>`; `""` → пустой `<Settings…/>` (список без ограничения типа); тип → с типом. Forgiving-синонимы: `typeDescription` (≈1С «ОписаниеТипов» / XML), `описаниеТипов`, `типЗначений`. Пример: `"valueType": "CatalogRef.Контрагенты"` | -| `savedData` | bool | Сохраняемые данные (`<SavedData>`) | +| `savedData` | bool | Сохраняемые данные (`<SavedData>`). **`false`** → суппресс авто-вывода компилятора (main-реквизит объектного типа Catalog/Document/ChartOf*/ExchangePlan/BusinessProcess/Task Object + RecordManager → `SavedData=true`). Нет ключа → авто-вывод | | `save` | bool/string/array | Сохранение значения в пользовательских настройках (`<Save><Field>…`). `true` → `<Field>имя</Field>`; строка/массив строк → под-поля с авто-префиксом `имя.` (путь с точкой / UUID `1/0:…` / совпадающее с именем — берётся как есть). Нет ключа или `false` → не эмитится. Пример периода: `["Период","EndDate","StartDate","Variant"]` | | `fillCheck` | bool/string | Проверка заполнения реквизита (`<FillCheck>`). `true` → `ShowError` (единственное значение в схеме); строка → verbatim. Синоним `fillChecking`. (`<FillChecking>` в схеме нет — был багом) | | `columns` | array | Колонки для ValueTable/ValueTree (`{ name, type, title?, functionalOptions?, useAlways? }`) | @@ -801,6 +802,7 @@ Pages поддерживает `pagesRepresentation`: `None`, `TabsOnTop`, `Tabs | `mainTable` | string | Основная таблица. Принимает рус-имена метаданных (`Справочник.X` → `Catalog.X`) | | `query` | string | Текст запроса (`ManualQuery=true`). Поддерживает `@file.sql` (путь относительно JSON) | | `dynamicDataRead` | bool | Динамическое считывание. **Умолчание `true`** — указывать только для отключения (`false`) | +| `autoFillAvailableFields` | bool | Автозаполнение доступных полей (`<AutoFillAvailableFields>`). **Умолчание `true`** — указывать только для отключения (`false`; тогда поля берутся из явного запроса, не авто). Эмитится первым в `<Settings>` | | `fields` | array | Явные поля набора (редко): `{ field, dataPath?, title? }` — для переопределения заголовка. Обычно поля выводятся из запроса автоматически | | `parameters` | array | Параметры схемы запроса (`DataCompositionSchemaParameter`) — см. ниже | | `order` | array | Сортировка списка (см. ниже) | diff --git a/tests/skills/cases/form-compile/dynamic-list-form.json b/tests/skills/cases/form-compile/dynamic-list-form.json index 14288aa7..b807341e 100644 --- a/tests/skills/cases/form-compile/dynamic-list-form.json +++ b/tests/skills/cases/form-compile/dynamic-list-form.json @@ -15,6 +15,7 @@ "validatePath": "Catalogs/Товары/Forms/ФормаСписка/Ext/Form.xml", "input": { "title": "Товары", + "properties": { "saveWindowSettings": false }, "attributes": [ { "name": "Список", "type": "DynamicList", "useAlways": ["~Артикул", "Список.Code", "Description"], "settings": { "mainTable": "Catalog.Товары", "dynamicDataRead": true, diff --git a/tests/skills/cases/form-compile/snapshots/dynamic-list-form/Catalogs/Товары/Forms/ФормаСписка/Ext/Form.xml b/tests/skills/cases/form-compile/snapshots/dynamic-list-form/Catalogs/Товары/Forms/ФормаСписка/Ext/Form.xml index d90d5dcd..e71ee343 100644 --- a/tests/skills/cases/form-compile/snapshots/dynamic-list-form/Catalogs/Товары/Forms/ФормаСписка/Ext/Form.xml +++ b/tests/skills/cases/form-compile/snapshots/dynamic-list-form/Catalogs/Товары/Forms/ФормаСписка/Ext/Form.xml @@ -7,6 +7,7 @@ </v8:item> false + false