feat(form-compile): нативная AutoCommandBar формы + autoCmdBar DSL

- Эвристика главной АКП: без cmdBar/autoCmdBar остаётся Autofill=true
  (как в Конфигураторе), с cmdBar — Autofill=false (обратная совместимость).
- Новый элемент autoCmdBar для наполнения главной АКП кастомными кнопками.
- Тихие синонимы commandBar↔cmdBar, autoCommandBar↔autoCmdBar.
- Инференс main-реквизита по типу (*Object.*, *RecordSet.*, DynamicList,
  ConstantsSet) — единственный кандидат проставляется молча с [INFO].
- Эвристика DynamicList → таблица: tableAutofill=false +
  commandBarLocation=None для привязанной таблицы (соответствие ERP).
- Косметика: <Autofill>true</Autofill> не эмитится явно.

Snapshot'ы form-* также обновлены до актуального состояния cf-init
(Ext/ClientApplicationInterface.xml).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-05-01 20:46:23 +03:00
co-authored by Claude Opus 4.7
parent 433a2955b5
commit 13174b63b1
93 changed files with 2936 additions and 41 deletions
+29 -1
View File
@@ -73,7 +73,8 @@ powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile
| `"picture"` | PictureDecoration | имя |
| `"picField"` | PictureField | имя |
| `"calendar"` | CalendarField | имя |
| `"cmdBar"` | CommandBar | имя |
| `"cmdBar"` | CommandBar | имя (синоним: `"commandBar"`) |
| `"autoCmdBar"` | AutoCommandBar формы | имя (синоним: `"autoCommandBar"`) — наполняет главную АКП формы (id=-1), не попадает в `<ChildItems>` |
| `"popup"` | Popup | имя |
### Общие свойства (все типы элементов)
@@ -202,11 +203,37 @@ powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile
### Командная панель (cmdBar)
Дополнительная пользовательская панель команд, размещается как обычный элемент в layout формы.
| Ключ | Описание |
|------|----------|
| `autofill: true` | Автозаполнение стандартными командами |
| `children: [...]` | Кнопки панели |
### Главная автокомандная панель формы (autoCmdBar)
Наполняет встроенную AutoCommandBar формы (id=-1) — ту самую верхнюю панель, на которой платформа автоматически размещает «Создать», «Записать», «Провести», «Заполнить» и т.п. По умолчанию `autoCmdBar` НЕ нужно указывать — компилятор сам решит, нужно ли отключать автозаполнение (см. «Эвристики» ниже). Используйте `autoCmdBar`, когда хотите добавить кастомные кнопки в эту главную панель или явно управлять автозаполнением.
| Ключ | Описание |
|------|----------|
| `autofill: true/false` | Явно включить/выключить автозаполнение стандартными командами |
| `horizontalAlign` | `"Left"` / `"Right"` (по умолчанию `"Right"`) |
| `children: [...]` | Кастомные кнопки/popup, добавляемые в главную панель |
```json
{ "autoCmdBar": "ФормаКоманднаяПанель", "autofill": true, "children": [
{ "button": "ИзменитьВыделенные", "command": "ИзменитьВыделенные",
"locationInCommandBar": "InAdditionalSubmenu" }
]}
```
### Эвристики компилятора (что делается автоматически)
1. **Автозаполнение главной АКП.** Если в DSL нет ни одного `cmdBar` и нет `autoCmdBar` — главная АКП формы оставляется с автозаполнением `true` (нативное поведение платформы; платформа сама насыпает стандартные команды). Если есть хотя бы один `cmdBar` — автозаполнение выключается, чтобы команды не дублировались. Перекрывается явным `autoCmdBar`.
2. **Главный реквизит выводится по типу.** Если ни у одного реквизита не указан `main: true`, компилятор пробует определить главный по типу: `*Object.*`, `*RecordSet.*`, `DynamicList`, `ConstantsSet`. Ровно один кандидат → проставляется `main: true` молча (с `[INFO]`-логом). Несколько → предупреждение, угадывания нет — нужно указать явно. Чтобы запретить инференс — поставьте `"main": false`.
3. **Динамический список → командная панель таблицы.** Если главный реквизит формы — `DynamicList`, то для `table` с `path`, равным имени этого реквизита, автоматически: `tableAutofill: false` и `commandBarLocation: "None"` (если не заданы явно). Это убирает дублирование команд между главной АКП и панелью таблицы — типовой паттерн форм списков.
4. **Косметика.** `<Autofill>true</Autofill>` не эмитится явно — соответствует поведению Конфигуратора и эталонным формам типовых.
### Выпадающее меню (popup)
| Ключ | Описание |
@@ -239,6 +266,7 @@ powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile
```
- `savedData: true` — сохраняемые данные
- `main: true` — главный реквизит формы. Если не указан ни у одного реквизита, компилятор сам выводит главный по типу: `*Object.*`, `*RecordSet.*`, `DynamicList`, `ConstantsSet`. Ровно один кандидат → проставляется молча. Несколько → нужно указать явно. Чтобы запретить инференс — поставьте `"main": false`.
### Команды (commands)
@@ -1,4 +1,4 @@
# form-compile v1.7 — Compile 1C managed form from JSON or object metadata
# form-compile v1.8 — Compile 1C managed form from JSON or object metadata
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[string]$JsonPath,
@@ -1823,6 +1823,17 @@ function Emit-Companion {
function Emit-Element {
param($el, [string]$indent)
# Silent synonyms: commandBar -> cmdBar, autoCommandBar -> autoCmdBar
# (autoCmdBar inside def.elements is normally extracted in pre-pass; this is a safety net for nested cases)
$synonyms = @{ "commandBar" = "cmdBar"; "autoCommandBar" = "autoCmdBar" }
foreach ($pair in $synonyms.GetEnumerator()) {
if ($null -ne $el.PSObject.Properties[$pair.Key] -and $null -eq $el.PSObject.Properties[$pair.Value]) {
$val = $el.($pair.Key)
$el.PSObject.Properties.Remove($pair.Key) | Out-Null
$el | Add-Member -NotePropertyName $pair.Value -NotePropertyValue $val -Force
}
}
# Determine element type from key
$typeKey = $null
$xmlTag = $null
@@ -2603,6 +2614,163 @@ function Emit-Properties {
}
}
# --- 11b. Pre-pass: synonyms, main attribute inference, heuristics, autoCmdBar extraction ---
function Normalize-ElementSynonyms {
param($el)
if ($null -eq $el) { return }
$synonyms = @{ "commandBar" = "cmdBar"; "autoCommandBar" = "autoCmdBar" }
foreach ($pair in $synonyms.GetEnumerator()) {
if ($null -ne $el.PSObject.Properties[$pair.Key] -and $null -eq $el.PSObject.Properties[$pair.Value]) {
$val = $el.($pair.Key)
$el.PSObject.Properties.Remove($pair.Key) | Out-Null
$el | Add-Member -NotePropertyName $pair.Value -NotePropertyValue $val -Force
}
}
if ($el.PSObject.Properties["children"] -and $el.children) {
foreach ($child in $el.children) { Normalize-ElementSynonyms $child }
}
if ($el.PSObject.Properties["columns"] -and $el.columns) {
foreach ($child in $el.columns) { Normalize-ElementSynonyms $child }
}
}
function HasCmdBarRecursive {
param($el)
if ($null -eq $el) { return $false }
if ($el.PSObject.Properties["cmdBar"] -and $null -ne $el.cmdBar) { return $true }
if ($el.PSObject.Properties["children"] -and $el.children) {
foreach ($child in $el.children) { if (HasCmdBarRecursive $child) { return $true } }
}
if ($el.PSObject.Properties["columns"] -and $el.columns) {
foreach ($child in $el.columns) { if (HasCmdBarRecursive $child) { return $true } }
}
return $false
}
function ApplyDynamicListTableHeuristic {
param($el, [string]$listName)
if ($null -eq $el) { return }
if ($el.PSObject.Properties["table"] -and $null -ne $el.table -and "$($el.path)" -eq $listName) {
if ($null -eq $el.PSObject.Properties["tableAutofill"]) {
$el | Add-Member -NotePropertyName "tableAutofill" -NotePropertyValue $false -Force
}
if ($null -eq $el.PSObject.Properties["commandBarLocation"]) {
$el | Add-Member -NotePropertyName "commandBarLocation" -NotePropertyValue "None" -Force
}
}
if ($el.PSObject.Properties["children"] -and $el.children) {
foreach ($child in $el.children) { ApplyDynamicListTableHeuristic $child $listName }
}
}
function Test-IsObjectLikeType {
param([string]$type)
if ([string]::IsNullOrEmpty($type)) { return $false }
if ($type -eq "DynamicList" -or $type -eq "ConstantsSet") { return $true }
$objectSuffixes = @(
"CatalogObject", "DocumentObject", "DataProcessorObject", "ReportObject",
"ExternalDataProcessorObject", "ExternalReportObject", "BusinessProcessObject",
"TaskObject", "ChartOfAccountsObject", "ChartOfCharacteristicTypesObject",
"ChartOfCalculationTypesObject", "ExchangePlanObject"
)
$recordSetPrefixes = @(
"InformationRegisterRecordSet", "AccumulationRegisterRecordSet",
"AccountingRegisterRecordSet", "CalculationRegisterRecordSet",
"InformationRegisterRecordManager"
)
foreach ($suffix in $objectSuffixes) {
if ($type -like "$suffix.*") { return $true }
}
foreach ($prefix in $recordSetPrefixes) {
if ($type -like "$prefix.*") { return $true }
}
return $false
}
# 11b.1: Normalize synonyms recursively
if ($def.elements) {
foreach ($el in $def.elements) { Normalize-ElementSynonyms $el }
}
# 11b.2: Extract autoCmdBar element from def.elements
$script:mainAcbDef = $null
if ($def.elements) {
$autoBars = @()
$rest = @()
foreach ($el in $def.elements) {
if ($null -ne $el.PSObject.Properties["autoCmdBar"] -and $null -ne $el.autoCmdBar) {
$autoBars += $el
} else {
$rest += $el
}
}
if ($autoBars.Count -gt 1) {
Write-Error "form-compile: more than one autoCmdBar in def.elements (found $($autoBars.Count)); only one allowed."
exit 1
}
if ($autoBars.Count -eq 1) {
$script:mainAcbDef = $autoBars[0]
# Replace def.elements with the filtered list
$def.PSObject.Properties.Remove("elements") | Out-Null
$def | Add-Member -NotePropertyName "elements" -NotePropertyValue $rest -Force
}
}
# 11b.3: Infer main attribute (only if no attribute has main:true)
if ($def.attributes) {
$hasExplicitMain = $false
foreach ($attr in $def.attributes) {
if ($attr.main -eq $true) { $hasExplicitMain = $true; break }
}
if (-not $hasExplicitMain) {
$candidates = @()
foreach ($attr in $def.attributes) {
# Skip if user explicitly opted out via main:false
if ($null -ne $attr.PSObject.Properties["main"] -and $attr.main -eq $false) { continue }
if (Test-IsObjectLikeType "$($attr.type)") {
$candidates += $attr
}
}
if ($candidates.Count -eq 1) {
$candidates[0] | Add-Member -NotePropertyName "main" -NotePropertyValue $true -Force
Write-Host "[INFO] Inferred main attribute: $($candidates[0].name) ($($candidates[0].type))"
} elseif ($candidates.Count -gt 1) {
$names = ($candidates | ForEach-Object { $_.name }) -join ", "
Write-Host "[WARN] Multiple main-attribute candidates: $names; specify ""main"": true explicitly"
}
}
}
# 11b.4: DynamicList → table heuristic
if ($def.attributes -and $def.elements) {
$mainAttr = $null
foreach ($attr in $def.attributes) {
if ($attr.main -eq $true) { $mainAttr = $attr; break }
}
if ($mainAttr -and "$($mainAttr.type)" -eq "DynamicList") {
foreach ($el in $def.elements) {
ApplyDynamicListTableHeuristic $el $mainAttr.name
}
}
}
# 11b.5: Compute main AutoCommandBar Autofill via heuristic B3
function Compute-MainAcbAutofill {
if ($script:mainAcbDef) {
if ($null -ne $script:mainAcbDef.PSObject.Properties["autofill"]) {
return [bool]$script:mainAcbDef.autofill
}
return $true
}
if ($def.elements) {
foreach ($el in $def.elements) {
if (HasCmdBarRecursive $el) { return $false }
}
}
return $true
}
# --- 12. Main compilation ---
# Title
@@ -2656,10 +2824,37 @@ if ($def.excludedCommands -and $def.excludedCommands.Count -gt 0) {
}
# 12d. AutoCommandBar (always present, id=-1)
X "`t<AutoCommandBar name=`"ФормаКоманднаяПанель`" id=`"-1`">"
X "`t`t<HorizontalAlign>Right</HorizontalAlign>"
X "`t`t<Autofill>false</Autofill>"
X "`t</AutoCommandBar>"
$acbAutofill = Compute-MainAcbAutofill
$acbName = "ФормаКоманднаяПанель"
$acbHAlign = "Right"
if ($script:mainAcbDef) {
if ($null -ne $script:mainAcbDef.PSObject.Properties["autoCmdBar"] -and "$($script:mainAcbDef.autoCmdBar)" -ne "") {
$acbName = "$($script:mainAcbDef.autoCmdBar)"
}
if ($null -ne $script:mainAcbDef.PSObject.Properties["name"] -and "$($script:mainAcbDef.name)" -ne "") {
$acbName = "$($script:mainAcbDef.name)"
}
if ($null -ne $script:mainAcbDef.PSObject.Properties["horizontalAlign"] -and "$($script:mainAcbDef.horizontalAlign)" -ne "") {
$acbHAlign = "$($script:mainAcbDef.horizontalAlign)"
}
}
$hasAcbChildren = ($script:mainAcbDef -and $script:mainAcbDef.children -and $script:mainAcbDef.children.Count -gt 0)
$acbHasInner = ($acbHAlign -or (-not $acbAutofill) -or $hasAcbChildren)
if ($acbHasInner) {
X "`t<AutoCommandBar name=`"$acbName`" id=`"-1`">"
if ($acbHAlign) { X "`t`t<HorizontalAlign>$acbHAlign</HorizontalAlign>" }
if (-not $acbAutofill) { X "`t`t<Autofill>false</Autofill>" }
if ($hasAcbChildren) {
X "`t`t<ChildItems>"
foreach ($child in $script:mainAcbDef.children) {
Emit-Element -el $child -indent "`t`t`t"
}
X "`t`t</ChildItems>"
}
X "`t</AutoCommandBar>"
} else {
X "`t<AutoCommandBar name=`"$acbName`" id=`"-1`"/>"
}
# 12e. Events
if ($def.events) {
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# form-compile v1.7 — Compile 1C managed form from JSON or object metadata
# form-compile v1.8 — Compile 1C managed form from JSON or object metadata
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import copy
@@ -1602,6 +1602,12 @@ def emit_type(lines, type_str, indent):
# --- Element emitters ---
def emit_element(lines, el, indent):
# Silent synonyms (safety net; top-level autoCmdBar is normally extracted in pre-pass)
_synonyms = {'commandBar': 'cmdBar', 'autoCommandBar': 'autoCmdBar'}
for src, dst in _synonyms.items():
if src in el and dst not in el:
el[dst] = el.pop(src)
type_key = None
for key in TYPE_KEYS:
if el.get(key) is not None:
@@ -2536,6 +2542,126 @@ def main():
with open(json_path, 'r', encoding='utf-8-sig') as f:
defn = json.load(f)
# --- 1b. Pre-pass: synonyms, main attribute inference, heuristics, autoCmdBar extraction ---
def _normalize_synonyms(el):
if not isinstance(el, dict):
return
synonyms = {'commandBar': 'cmdBar', 'autoCommandBar': 'autoCmdBar'}
for src, dst in synonyms.items():
if src in el and dst not in el:
el[dst] = el.pop(src)
if isinstance(el.get('children'), list):
for child in el['children']:
_normalize_synonyms(child)
if isinstance(el.get('columns'), list):
for child in el['columns']:
_normalize_synonyms(child)
def _has_cmd_bar_recursive(el):
if not isinstance(el, dict):
return False
if el.get('cmdBar') is not None:
return True
if isinstance(el.get('children'), list):
for child in el['children']:
if _has_cmd_bar_recursive(child):
return True
if isinstance(el.get('columns'), list):
for child in el['columns']:
if _has_cmd_bar_recursive(child):
return True
return False
def _apply_dlist_table_heuristic(el, list_name):
if not isinstance(el, dict):
return
if el.get('table') is not None and str(el.get('path', '')) == list_name:
if 'tableAutofill' not in el:
el['tableAutofill'] = False
if 'commandBarLocation' not in el:
el['commandBarLocation'] = 'None'
if isinstance(el.get('children'), list):
for child in el['children']:
_apply_dlist_table_heuristic(child, list_name)
def _is_object_like_type(t):
if not t:
return False
if t == 'DynamicList' or t == 'ConstantsSet':
return True
object_suffixes = (
'CatalogObject', 'DocumentObject', 'DataProcessorObject', 'ReportObject',
'ExternalDataProcessorObject', 'ExternalReportObject', 'BusinessProcessObject',
'TaskObject', 'ChartOfAccountsObject', 'ChartOfCharacteristicTypesObject',
'ChartOfCalculationTypesObject', 'ExchangePlanObject',
)
record_set_prefixes = (
'InformationRegisterRecordSet', 'AccumulationRegisterRecordSet',
'AccountingRegisterRecordSet', 'CalculationRegisterRecordSet',
'InformationRegisterRecordManager',
)
for s in object_suffixes:
if t.startswith(s + '.'):
return True
for s in record_set_prefixes:
if t.startswith(s + '.'):
return True
return False
# 1b.1: Normalize synonyms recursively
if isinstance(defn.get('elements'), list):
for el in defn['elements']:
_normalize_synonyms(el)
# 1b.2: Extract autoCmdBar element from defn['elements']
main_acb_def = None
if isinstance(defn.get('elements'), list):
auto_bars = [el for el in defn['elements'] if isinstance(el, dict) and el.get('autoCmdBar') is not None]
if len(auto_bars) > 1:
print(f"form-compile: more than one autoCmdBar in def.elements (found {len(auto_bars)}); only one allowed.", file=sys.stderr)
sys.exit(1)
if len(auto_bars) == 1:
main_acb_def = auto_bars[0]
defn['elements'] = [el for el in defn['elements'] if el is not main_acb_def]
# 1b.3: Infer main attribute
if isinstance(defn.get('attributes'), list):
has_explicit_main = any(a.get('main') is True for a in defn['attributes'] if isinstance(a, dict))
if not has_explicit_main:
candidates = []
for a in defn['attributes']:
if not isinstance(a, dict):
continue
if 'main' in a and a.get('main') is False:
continue
if _is_object_like_type(str(a.get('type', ''))):
candidates.append(a)
if len(candidates) == 1:
candidates[0]['main'] = True
print(f"[INFO] Inferred main attribute: {candidates[0].get('name')} ({candidates[0].get('type')})")
elif len(candidates) > 1:
names = ', '.join(c.get('name', '') for c in candidates)
print(f"[WARN] Multiple main-attribute candidates: {names}; specify \"main\": true explicitly")
# 1b.4: DynamicList → table heuristic
if isinstance(defn.get('attributes'), list) and isinstance(defn.get('elements'), list):
main_attr = next((a for a in defn['attributes'] if isinstance(a, dict) and a.get('main') is True), None)
if main_attr and str(main_attr.get('type', '')) == 'DynamicList':
for el in defn['elements']:
_apply_dlist_table_heuristic(el, main_attr.get('name', ''))
# 1b.5: Compute main AutoCommandBar Autofill (B3)
def _compute_main_acb_autofill():
if main_acb_def is not None:
if 'autofill' in main_acb_def:
return bool(main_acb_def.get('autofill'))
return True
if isinstance(defn.get('elements'), list):
for el in defn['elements']:
if _has_cmd_bar_recursive(el):
return False
return True
# --- 2. Main compilation ---
_next_id = 0
lines = []
@@ -2563,10 +2689,33 @@ def main():
lines.append('\t</CommandSet>')
# AutoCommandBar (always present, id=-1)
lines.append('\t<AutoCommandBar name="\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c" id="-1">')
lines.append('\t\t<HorizontalAlign>Right</HorizontalAlign>')
lines.append('\t\t<Autofill>false</Autofill>')
lines.append('\t</AutoCommandBar>')
acb_autofill = _compute_main_acb_autofill()
acb_name = '\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c'
acb_halign = 'Right'
if main_acb_def is not None:
v = main_acb_def.get('autoCmdBar')
if v:
acb_name = str(v)
if main_acb_def.get('name'):
acb_name = str(main_acb_def['name'])
if main_acb_def.get('horizontalAlign'):
acb_halign = str(main_acb_def['horizontalAlign'])
has_acb_children = bool(main_acb_def and isinstance(main_acb_def.get('children'), list) and len(main_acb_def['children']) > 0)
has_inner = bool(acb_halign) or (not acb_autofill) or has_acb_children
if has_inner:
lines.append(f'\t<AutoCommandBar name="{acb_name}" id="-1">')
if acb_halign:
lines.append(f'\t\t<HorizontalAlign>{acb_halign}</HorizontalAlign>')
if not acb_autofill:
lines.append('\t\t<Autofill>false</Autofill>')
if has_acb_children:
lines.append('\t\t<ChildItems>')
for child in main_acb_def['children']:
emit_element(lines, child, '\t\t\t')
lines.append('\t\t</ChildItems>')
lines.append('\t</AutoCommandBar>')
else:
lines.append(f'\t<AutoCommandBar name="{acb_name}" id="-1"/>')
# Events
if defn.get('events'):