From 580124a9710adb38d594d7ffcaf7995005168ed3 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Mon, 9 Feb 2026 11:09:42 +0300 Subject: [PATCH 01/13] Add form-info skill for compact analysis of 1C managed forms Parses Form.xml (up to 28K lines) and outputs a compact summary (40-180 lines): element tree with group orientation, data bindings, events, visibility flags, attributes with types, commands, and parameters. Co-Authored-By: Claude Opus 4.6 --- .claude/skills/form-info/SKILL.md | 178 ++++++ .../skills/form-info/scripts/form-info.ps1 | 511 ++++++++++++++++++ README.md | 3 + docs/form-guide.md | 141 +++++ 4 files changed, 833 insertions(+) create mode 100644 .claude/skills/form-info/SKILL.md create mode 100644 .claude/skills/form-info/scripts/form-info.ps1 create mode 100644 docs/form-guide.md diff --git a/.claude/skills/form-info/SKILL.md b/.claude/skills/form-info/SKILL.md new file mode 100644 index 00000000..eed56b7e --- /dev/null +++ b/.claude/skills/form-info/SKILL.md @@ -0,0 +1,178 @@ +--- +name: form-info +description: Анализ структуры управляемой формы 1С (Form.xml) — элементы, реквизиты, команды, события +argument-hint: +allowed-tools: + - Bash + - Read + - Glob +--- + +# /form-info — Компактная сводка формы + +Читает Form.xml управляемой формы и выводит компактную сводку: дерево элементов, реквизиты с типами, команды, события. Заменяет необходимость читать тысячи строк XML. + +## Использование + +``` +/form-info +``` + +## Параметры + +| Параметр | Обязательный | По умолчанию | Описание | +|-----------|:------------:|--------------|---------------------------------------------| +| FormPath | да | — | Путь к файлу Form.xml | +| Limit | нет | `150` | Макс. строк вывода (защита от переполнения) | +| Offset | нет | `0` | Пропустить N строк (для пагинации) | + +## Команда + +```powershell +powershell.exe -NoProfile -File .claude\skills\form-info\scripts\form-info.ps1 -FormPath "<путь к Form.xml>" +``` + +С пагинацией: +```powershell +powershell.exe -NoProfile -File .claude\skills\form-info\scripts\form-info.ps1 -FormPath "<путь>" -Offset 150 +``` + +## Чтение вывода + +### Заголовок + +``` +=== Form: ФормаДокумента (Documents.РеализацияТоваровУслуг) === +``` + +Имя формы и контекст объекта определяются из пути к файлу. + +### Properties — свойства формы + +Только нестандартные свойства (отличающиеся от умолчания): + +``` +Properties: AutoTitle=false, WindowOpeningMode=LockOwnerWindow, CommandBarLocation=Bottom +``` + +### Events — обработчики событий формы + +``` +Events: + OnCreateAtServer -> ПриСозданииНаСервере + OnOpen -> ПриОткрытии +``` + +### Elements — дерево UI-элементов + +Компактное дерево с типами, привязками к данным, флагами и событиями: + +``` +Elements: + ├─ [Group:AH] ГруппаШапка + │ ├─ [Input] Организация -> Объект.Организация {OnChange} + │ └─ [Input] Договор -> Объект.Договор [hidden] {StartChoice} + ├─ [Table] Товары -> Объект.Товары + │ ├─ [Input] Номенклатура -> Объект.Товары.Номенклатура {OnChange} + │ └─ [Input] Сумма -> Объект.Товары.Сумма [ro] + └─ [Pages] Страницы + ├─ [Page] Основное (5 items) + └─ [Page] Печать (2 items) +``` + +**Сокращения типов элементов:** + +| Сокращение | Элемент | +|---|---| +| `[Group:V]` | UsualGroup Vertical | +| `[Group:H]` | UsualGroup Horizontal | +| `[Group:AH]` | UsualGroup AlwaysHorizontal | +| `[Group:AV]` | UsualGroup AlwaysVertical | +| `[Group]` | UsualGroup (ориентация по умолчанию) | +| `[Input]` | InputField | +| `[Check]` | CheckBoxField | +| `[Label]` | LabelDecoration | +| `[LabelField]` | LabelField | +| `[Picture]` | PictureDecoration | +| `[PicField]` | PictureField | +| `[Calendar]` | CalendarField | +| `[Table]` | Table | +| `[Button]` | Button | +| `[CmdBar]` | CommandBar | +| `[Pages]` | Pages | +| `[Page]` | Page (показывает кол-во элементов вместо раскрытия) | +| `[Popup]` | Popup | +| `[BtnGroup]` | ButtonGroup | + +**Флаги** (только при отклонении от умолчания): +- `[hidden]` — Visible=false +- `[disabled]` — Enabled=false +- `[ro]` — ReadOnly=true +- `,collapse` — Behavior=Collapsible (для групп) + +**Привязка**: `-> Объект.Поле` — DataPath или CommandName + +**События**: `{OnChange, StartChoice}` — имена обработчиков + +**Заголовок**: `[title:Текст]` — только если отличается от имени элемента + +### Attributes — реквизиты формы + +``` +Attributes: + *Объект: DocumentObject.РеализацияТоваров (main) + Валюта: CatalogRef.Валюты + Итого: decimal(15,2) + Таблица: ValueTable [Номенклатура: CatalogRef.Номенклатура, Кол: decimal(10,3)] + Список: DynamicList -> Catalog.Пользователи +``` + +- `*` и `(main)` — основной реквизит формы (MainAttribute) +- Типы ValueTable/ValueTree раскрывают колонки в `[...]` +- DynamicList показывает MainTable через `->` + +### Parameters — параметры формы + +``` +Parameters: + Ключ: DocumentRef.ЗакупкаТоваров (key) + Основание: DocumentRef.* +``` + +- `(key)` — ключевой параметр (KeyParameter) + +### Commands — команды формы + +``` +Commands: + Печать -> ПечатьДокумента [Ctrl+P] + Заполнить -> ЗаполнитьОбработка +``` + +Формат: `Имя -> Обработчик [Сочетание]` + +## Что пропускается + +Скрипт убирает 80%+ XML-объёма: +- Визуальные свойства (Width, Height, Color, Font, Border, Align, Stretch) +- Автогенерированные ExtendedTooltip и ContextMenu +- Мультиязычные обёртки (v8:item/v8:lang/v8:content) +- Namespace-декларации +- Атрибуты id + +Для точечного изучения деталей — используйте grep по имени элемента из сводки. + +## Когда использовать + +- **Перед модификацией формы**: понять структуру, найти нужную группу для вставки элемента +- **Анализ формы**: какие реквизиты, команды, обработчики задействованы +- **Навигация по большим формам**: 28K строк XML → 50-100 строк контекста + +## Защита от переполнения + +Вывод ограничен 150 строками по умолчанию. При превышении: +``` +[TRUNCATED] Shown 150 of 220 lines. Use -Offset 150 to continue. +``` + +Используйте `-Offset N` и `-Limit N` для постраничного просмотра. diff --git a/.claude/skills/form-info/scripts/form-info.ps1 b/.claude/skills/form-info/scripts/form-info.ps1 new file mode 100644 index 00000000..459a51f9 --- /dev/null +++ b/.claude/skills/form-info/scripts/form-info.ps1 @@ -0,0 +1,511 @@ +param( + [Parameter(Mandatory=$true)] + [string]$FormPath, + [int]$Limit = 150, + [int]$Offset = 0 +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Validate path --- + +if (-not (Test-Path $FormPath)) { + Write-Error "File not found: $FormPath" + exit 1 +} + +# --- Load XML --- + +$xmlDoc = New-Object System.Xml.XmlDocument +$xmlDoc.PreserveWhitespace = $false +$xmlDoc.Load((Resolve-Path $FormPath).Path) + +$ns = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) +$ns.AddNamespace("d", "http://v8.1c.ru/8.3/xcf/logform") +$ns.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core") +$ns.AddNamespace("v8ui", "http://v8.1c.ru/8.1/data/ui") +$ns.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable") +$ns.AddNamespace("xs", "http://www.w3.org/2001/XMLSchema") +$ns.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance") +$ns.AddNamespace("cfg", "http://v8.1c.ru/8.1/data/enterprise/current-config") +$ns.AddNamespace("dcsset", "http://v8.1c.ru/8.1/data-composition-system/settings") + +$root = $xmlDoc.DocumentElement + +# --- Helper: extract multilang text --- + +function Get-MLText($node) { + if (-not $node) { return "" } + $content = $node.SelectSingleNode("v8:item/v8:content", $ns) + if ($content) { return $content.InnerText } + $text = $node.InnerText.Trim() + if ($text) { return $text } + return "" +} + +# --- Helper: format type compactly --- + +function Format-Type($typeNode) { + if (-not $typeNode -or -not $typeNode.HasChildNodes) { return "" } + + $typeSet = $typeNode.SelectSingleNode("v8:TypeSet", $ns) + if ($typeSet) { + $val = $typeSet.InnerText + # Strip cfg: prefix for DefinedType, keep as-is + if ($val -like "cfg:*") { $val = $val.Substring(4) } + return $val + } + + $types = $typeNode.SelectNodes("v8:Type", $ns) + if ($types.Count -eq 0) { return "" } + + $parts = @() + foreach ($t in $types) { + $raw = $t.InnerText + switch -Wildcard ($raw) { + "xs:string" { + $sq = $typeNode.SelectSingleNode("v8:StringQualifiers/v8:Length", $ns) + $len = if ($sq) { [int]$sq.InnerText } else { 0 } + if ($len -gt 0) { $parts += "string($len)" } else { $parts += "string" } + } + "xs:decimal" { + $nq = $typeNode.SelectSingleNode("v8:NumberQualifiers", $ns) + if ($nq) { + $d = $nq.SelectSingleNode("v8:Digits", $ns) + $f = $nq.SelectSingleNode("v8:FractionDigits", $ns) + $digits = if ($d) { $d.InnerText } else { "0" } + $frac = if ($f) { $f.InnerText } else { "0" } + $parts += "decimal($digits,$frac)" + } else { + $parts += "decimal" + } + } + "xs:boolean" { $parts += "boolean" } + "xs:dateTime" { + $dq = $typeNode.SelectSingleNode("v8:DateQualifiers/v8:DateFractions", $ns) + if ($dq) { + switch ($dq.InnerText) { + "Date" { $parts += "date" } + "Time" { $parts += "time" } + default { $parts += "dateTime" } + } + } else { + $parts += "dateTime" + } + } + "xs:binary" { $parts += "binary" } + "cfg:*" { $parts += $raw.Substring(4) } + "v8:ValueTable" { $parts += "ValueTable" } + "v8:ValueTree" { $parts += "ValueTree" } + "v8:ValueListType" { $parts += "ValueList" } + "v8:TypeDescription" { $parts += "TypeDescription" } + "v8:Universal" { $parts += "Universal" } + "v8:FixedArray" { $parts += "FixedArray" } + "v8:FixedStructure" { $parts += "FixedStructure" } + "v8ui:FormattedString" { $parts += "FormattedString" } + "v8ui:Picture" { $parts += "Picture" } + "v8ui:Color" { $parts += "Color" } + "v8ui:Font" { $parts += "Font" } + "dcsset:*" { $parts += $raw.Replace("dcsset:", "DCS.") } + "dcssch:*" { $parts += $raw.Replace("dcssch:", "DCS.") } + "dcscor:*" { $parts += $raw.Replace("dcscor:", "DCS.") } + default { $parts += $raw } + } + } + + return ($parts -join " | ") +} + +# --- Helper: check if title differs from name --- + +function Test-TitleDiffers($node, [string]$name) { + $titleNode = $node.SelectSingleNode("d:Title", $ns) + if (-not $titleNode) { return $null } + $titleText = Get-MLText $titleNode + if (-not $titleText) { return $null } + # Normalize: remove spaces, lowercase + $normTitle = ($titleText -replace '\s', '').ToLower() + $normName = $name.ToLower() + if ($normTitle -eq $normName) { return $null } + return $titleText +} + +# --- Helper: get events as compact string --- + +function Get-EventsStr($node) { + $eventsNode = $node.SelectSingleNode("d:Events", $ns) + if (-not $eventsNode) { return "" } + $evts = @() + foreach ($e in $eventsNode.SelectNodes("d:Event", $ns)) { + $evts += $e.GetAttribute("name") + } + if ($evts.Count -eq 0) { return "" } + return " {$($evts -join ', ')}" +} + +# --- Helper: get flags --- + +function Get-Flags($node) { + $flags = @() + $vis = $node.SelectSingleNode("d:Visible", $ns) + if ($vis -and $vis.InnerText -eq "false") { $flags += "hidden" } + $en = $node.SelectSingleNode("d:Enabled", $ns) + if ($en -and $en.InnerText -eq "false") { $flags += "disabled" } + $ro = $node.SelectSingleNode("d:ReadOnly", $ns) + if ($ro -and $ro.InnerText -eq "true") { $flags += "ro" } + if ($flags.Count -eq 0) { return "" } + return " [$($flags -join ',')]" +} + +# --- Element type abbreviations --- + +$skipElements = @{ + "ExtendedTooltip" = $true + "ContextMenu" = $true + "AutoCommandBar" = $true + "SearchStringAddition" = $true + "ViewStatusAddition" = $true + "SearchControlAddition" = $true + "ColumnGroup" = $true +} + +function Get-ElementTag($node) { + $localName = $node.LocalName + switch ($localName) { + "UsualGroup" { + $groupNode = $node.SelectSingleNode("d:Group", $ns) + $orient = "" + if ($groupNode) { + switch ($groupNode.InnerText) { + "Vertical" { $orient = ":V" } + "Horizontal" { $orient = ":H" } + "AlwaysHorizontal" { $orient = ":AH" } + "AlwaysVertical" { $orient = ":AV" } + } + } + $beh = $node.SelectSingleNode("d:Behavior", $ns) + $collapse = "" + if ($beh -and $beh.InnerText -eq "Collapsible") { $collapse = ",collapse" } + return "[Group$orient$collapse]" + } + "InputField" { return "[Input]" } + "CheckBoxField" { return "[Check]" } + "LabelDecoration" { return "[Label]" } + "LabelField" { return "[LabelField]" } + "PictureDecoration" { return "[Picture]" } + "PictureField" { return "[PicField]" } + "CalendarField" { return "[Calendar]" } + "Table" { return "[Table]" } + "Button" { return "[Button]" } + "CommandBar" { return "[CmdBar]" } + "Pages" { return "[Pages]" } + "Page" { return "[Page]" } + "Popup" { return "[Popup]" } + "ButtonGroup" { return "[BtnGroup]" } + default { return "[$localName]" } + } +} + +# --- Count significant children (for Page summary) --- + +function Count-SignificantChildren($childItemsNode) { + if (-not $childItemsNode) { return 0 } + $count = 0 + foreach ($child in $childItemsNode.ChildNodes) { + if ($child.NodeType -ne "Element") { continue } + if ($skipElements.ContainsKey($child.LocalName)) { continue } + $count++ + } + return $count +} + +# --- Build element tree recursively --- + +$treeLines = [System.Collections.Generic.List[string]]::new() + +function Build-Tree($childItemsNode, [string]$prefix, [bool]$isLast) { + if (-not $childItemsNode) { return } + + # Collect significant children + $children = @() + foreach ($child in $childItemsNode.ChildNodes) { + if ($child.NodeType -ne "Element") { continue } + if ($skipElements.ContainsKey($child.LocalName)) { continue } + $children += $child + } + + for ($i = 0; $i -lt $children.Count; $i++) { + $child = $children[$i] + $last = ($i -eq $children.Count - 1) + $connector = if ($last) { [char]0x2514 + [string][char]0x2500 } else { [char]0x251C + [string][char]0x2500 } + $continuation = if ($last) { " " } else { [string][char]0x2502 + " " } + + $tag = Get-ElementTag $child + $name = $child.GetAttribute("name") + $flags = Get-Flags $child + $events = Get-EventsStr $child + + # DataPath or CommandName + $binding = "" + $dp = $child.SelectSingleNode("d:DataPath", $ns) + if ($dp) { + $binding = " -> $($dp.InnerText)" + } else { + $cn = $child.SelectSingleNode("d:CommandName", $ns) + if ($cn) { $binding = " -> $($cn.InnerText)" } + } + + # Title differs? + $titleStr = "" + $diffTitle = Test-TitleDiffers $child $name + if ($diffTitle) { $titleStr = " [title:$diffTitle]" } + + $line = "$prefix$connector $tag $name$binding$flags$titleStr$events" + $treeLines.Add($line) + + # Recurse into containers (but not Page — show summary) + $localName = $child.LocalName + if ($localName -eq "Page") { + $ci = $child.SelectSingleNode("d:ChildItems", $ns) + $cnt = Count-SignificantChildren $ci + # Append count to last line + $idx = $treeLines.Count - 1 + $treeLines[$idx] = $treeLines[$idx] + " ($cnt items)" + } elseif ($localName -in @("UsualGroup", "Pages", "Table", "CommandBar", "ButtonGroup", "Popup")) { + $ci = $child.SelectSingleNode("d:ChildItems", $ns) + if ($ci) { + Build-Tree $ci "$prefix$continuation" $last + } + } + } +} + +# --- Determine form name and object from path --- + +$resolvedPath = (Resolve-Path $FormPath).Path +$parts = $resolvedPath -split '[/\\]' + +$formName = "" +$objectContext = "" + +# Look for /Forms//Ext/Form.xml pattern +$formsIdx = -1 +for ($i = $parts.Count - 1; $i -ge 0; $i--) { + if ($parts[$i] -eq "Forms") { $formsIdx = $i; break } +} + +if ($formsIdx -ge 0 -and ($formsIdx + 1) -lt $parts.Count) { + $formName = $parts[$formsIdx + 1] + # Object is 2 levels up: ...///Forms/... + if ($formsIdx -ge 2) { + $objType = $parts[$formsIdx - 2] + $objName = $parts[$formsIdx - 1] + $objectContext = "$objType.$objName" + } +} else { + # CommonForms pattern: ...///Ext/Form.xml + $extIdx = -1 + for ($i = $parts.Count - 1; $i -ge 0; $i--) { + if ($parts[$i] -eq "Ext") { $extIdx = $i; break } + } + if ($extIdx -ge 2) { + $formName = $parts[$extIdx - 1] + $objType = $parts[$extIdx - 2] + $objectContext = $objType + } else { + $formName = [System.IO.Path]::GetFileNameWithoutExtension($FormPath) + } +} + +# --- Collect output --- + +$lines = @() + +# Header +$header = "=== Form: $formName" +if ($objectContext) { $header += " ($objectContext)" } +$header += " ===" +$lines += $header + +# --- Form properties --- + +$propNames = @( + "Title", "Width", "Height", "Group", + "WindowOpeningMode", "EnterKeyBehavior", "AutoTitle", "AutoURL", + "AutoFillCheck", "Customizable", "CommandBarLocation", + "SaveDataInSettings", "AutoSaveDataInSettings", + "AutoTime", "UsePostingMode", "RepostOnWrite", + "UseForFoldersAndItems", + "ReportResult", "DetailsData", "ReportFormType", + "VerticalScroll", "ScalingMode" +) + +$props = @() +foreach ($pn in $propNames) { + $pNode = $root.SelectSingleNode("d:$pn", $ns) + if ($pNode) { + $val = Get-MLText $pNode + if (-not $val) { $val = $pNode.InnerText } + $props += "$pn=$val" + } +} + +if ($props.Count -gt 0) { + $lines += "" + $lines += "Properties: $($props -join ', ')" +} + +# --- Excluded commands --- + +$excludedCmds = @() +foreach ($ec in $root.SelectNodes("d:CommandSet/d:ExcludedCommand", $ns)) { + $excludedCmds += $ec.InnerText +} + +# --- Form events --- + +$formEvents = $root.SelectSingleNode("d:Events", $ns) +if ($formEvents -and $formEvents.HasChildNodes) { + $lines += "" + $lines += "Events:" + foreach ($e in $formEvents.SelectNodes("d:Event", $ns)) { + $eName = $e.GetAttribute("name") + $eHandler = $e.InnerText + $lines += " $eName -> $eHandler" + } +} + +# --- Element tree --- + +$childItems = $root.SelectSingleNode("d:ChildItems", $ns) +if ($childItems) { + $lines += "" + $lines += "Elements:" + Build-Tree $childItems " " $false + $lines += $treeLines.ToArray() +} + +# --- Attributes --- + +$attrsNode = $root.SelectSingleNode("d:Attributes", $ns) +if ($attrsNode) { + $attrLines = @() + foreach ($attr in $attrsNode.SelectNodes("d:Attribute", $ns)) { + $aName = $attr.GetAttribute("name") + $typeNode = $attr.SelectSingleNode("d:Type", $ns) + $typeStr = Format-Type $typeNode + + $mainAttr = $attr.SelectSingleNode("d:MainAttribute", $ns) + $isMain = ($mainAttr -and $mainAttr.InnerText -eq "true") + + $prefix = if ($isMain) { "*" } else { " " } + $mainSuffix = if ($isMain) { " (main)" } else { "" } + + # DynamicList: show MainTable + $settings = $attr.SelectSingleNode("d:Settings", $ns) + $dynTable = "" + if ($settings -and $typeStr -eq "DynamicList") { + $mt = $settings.SelectSingleNode("d:MainTable", $ns) + if ($mt) { $dynTable = " -> $($mt.InnerText)" } + } + + # ValueTable/ValueTree columns + $colStr = "" + $columns = $attr.SelectSingleNode("d:Columns", $ns) + if ($columns -and ($typeStr -eq "ValueTable" -or $typeStr -eq "ValueTree")) { + $cols = @() + foreach ($col in $columns.SelectNodes("d:Column", $ns)) { + $cName = $col.GetAttribute("name") + $cTypeNode = $col.SelectSingleNode("d:Type", $ns) + $cType = Format-Type $cTypeNode + if ($cType) { $cols += "$cName`: $cType" } else { $cols += $cName } + } + if ($cols.Count -gt 0) { + $colStr = " [$($cols -join ', ')]" + } + } + + $line = " $prefix$aName`: $typeStr$colStr$dynTable$mainSuffix" + if (-not $typeStr -and -not $colStr -and -not $dynTable) { + $line = " $prefix$aName$mainSuffix" + } + $attrLines += $line + } + if ($attrLines.Count -gt 0) { + $lines += "" + $lines += "Attributes:" + $lines += $attrLines + } +} + +# --- Parameters --- + +$paramsNode = $root.SelectSingleNode("d:Parameters", $ns) +if ($paramsNode) { + $paramLines = @() + foreach ($param in $paramsNode.SelectNodes("d:Parameter", $ns)) { + $pName = $param.GetAttribute("name") + $typeNode = $param.SelectSingleNode("d:Type", $ns) + $typeStr = Format-Type $typeNode + + $keyParam = $param.SelectSingleNode("d:KeyParameter", $ns) + $isKey = ($keyParam -and $keyParam.InnerText -eq "true") + $keySuffix = if ($isKey) { " (key)" } else { "" } + + if ($typeStr) { + $paramLines += " $pName`: $typeStr$keySuffix" + } else { + $paramLines += " $pName$keySuffix" + } + } + if ($paramLines.Count -gt 0) { + $lines += "" + $lines += "Parameters:" + $lines += $paramLines + } +} + +# --- Commands --- + +$cmdsNode = $root.SelectSingleNode("d:Commands", $ns) +if ($cmdsNode) { + $cmdLines = @() + foreach ($cmd in $cmdsNode.SelectNodes("d:Command", $ns)) { + $cName = $cmd.GetAttribute("name") + $action = $cmd.SelectSingleNode("d:Action", $ns) + $shortcut = $cmd.SelectSingleNode("d:Shortcut", $ns) + + $actionStr = if ($action) { " -> $($action.InnerText)" } else { "" } + $scStr = if ($shortcut) { " [$($shortcut.InnerText)]" } else { "" } + + $cmdLines += " $cName$actionStr$scStr" + } + if ($cmdLines.Count -gt 0) { + $lines += "" + $lines += "Commands:" + $lines += $cmdLines + } +} + +# --- Truncation protection --- + +$totalLines = $lines.Count + +if ($Offset -gt 0) { + if ($Offset -ge $totalLines) { + Write-Host "[INFO] Offset $Offset exceeds total lines ($totalLines). Nothing to show." + exit 0 + } + $lines = $lines[$Offset..($totalLines - 1)] +} + +if ($lines.Count -gt $Limit) { + $shown = $lines[0..($Limit - 1)] + foreach ($l in $shown) { Write-Host $l } + $remaining = $totalLines - $Offset - $Limit + Write-Host "" + Write-Host "[TRUNCATED] Shown $Limit of $totalLines lines. Use -Offset $($Offset + $Limit) to continue." +} else { + foreach ($l in $lines) { Write-Host $l } +} diff --git a/README.md b/README.md index f42a918f..3e402407 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ |--------|--------|----------|------| | Внешние обработки (EPF) | 10 навыков `/epf-*` | Создание, модификация, сборка обработок из XML-исходников | [Подробнее](docs/epf-guide.md) | | Табличный документ (MXL) | 4 навыка `/mxl-*` | Анализ, создание, компиляция макетов печатных форм | [Подробнее](docs/mxl-guide.md) | +| Управляемые формы (Form) | `/form-info` | Анализ структуры управляемых форм из XML-исходников конфигурации | [Подробнее](docs/form-guide.md) | | Утилиты | `/img-grid` | Наложение сетки на изображение для определения пропорций колонок | — | ## Требования @@ -55,10 +56,12 @@ ├── mxl-validate/ # Валидация макета ├── mxl-compile/ # Компиляция макета из JSON ├── mxl-decompile/ # Декомпиляция макета в JSON +├── form-info/ # Анализ структуры управляемой формы └── img-grid/ # Сетка для анализа изображений docs/ ├── epf-guide.md # Гайд: внешние обработки ├── mxl-guide.md # Гайд: табличный документ +├── form-guide.md # Гайд: управляемые формы ├── 1c-xml-format-spec.md # Спецификация XML-формата ├── 1c-form-spec.md # Спецификация управляемых форм ├── 1c-help-spec.md # Спецификация встроенной справки diff --git a/docs/form-guide.md b/docs/form-guide.md new file mode 100644 index 00000000..cbc4e7ee --- /dev/null +++ b/docs/form-guide.md @@ -0,0 +1,141 @@ +# Управляемые формы (Form) + +Навыки группы `/form-*` позволяют анализировать управляемые формы 1С:Предприятия 8.3 из XML-исходников конфигурации, не читая тысячи строк XML. + +## Навыки + +| Навык | Параметры | Описание | +|-------|-----------|----------| +| `/form-info` | `` | Компактная сводка: дерево элементов, реквизиты, команды, события | + +## Сценарии использования + +### Анализ формы перед модификацией + +Файлы Form.xml содержат от сотен до десятков тысяч строк XML. 80% объёма — визуальный шум (цвета, шрифты, размеры, автогенерированные подсказки). `/form-info` извлекает суть. + +``` +> Покажи структуру формы документа РеализацияТоваровУслуг +``` + +Claude найдёт Form.xml и вызовет `/form-info`. Результат — компактная сводка (40–100 строк вместо тысяч): + +``` +=== Form: ФормаДокумента (Documents.РеализацияТоваровУслуг) === + +Properties: AutoTitle=false, CommandBarLocation=None + +Events: + OnCreateAtServer -> ПриСозданииНаСервере + OnOpen -> ПриОткрытии + +Elements: + ├─ [Group:AH] ГруппаШапка + │ ├─ [Input] Организация -> Объект.Организация {OnChange} + │ └─ [Input] Контрагент -> Объект.Контрагент {OnChange, StartChoice} + ├─ [Table] Товары -> Объект.Товары + │ ├─ [Input] Номенклатура -> Объект.Товары.Номенклатура {OnChange} + │ └─ [Input] Сумма -> Объект.Товары.Сумма [ro] + └─ [Pages] Страницы + ├─ [Page] Основное (5 items) + └─ [Page] Печать (2 items) + +Attributes: + *Объект: DocumentObject.РеализацияТоваровУслуг (main) + Валюта: CatalogRef.Валюты + ... + +Commands: + Печать -> ПечатьДокумента [Ctrl+P] +``` + +### Добавление элемента на форму + +Чтобы добавить поле на форму, нужно знать структуру групп и ориентацию (горизонтальная/вертикальная). `/form-info` показывает это в дереве элементов. + +``` +> Добавь поле "Склад" в шапку формы документа +``` + +Claude вызовет `/form-info`, увидит `[Group:AH] ГруппаШапка` → `[Group:V] ГруппаШапкаЛевая`, поймёт куда вставить элемент в XML. + +### Поиск обработчиков и привязок + +Из сводки видно, какие события подключены к каким элементам: + +``` +> Какие обработчики срабатывают при изменении контрагента? +``` + +Claude вызовет `/form-info` и найдёт: `[Input] Контрагент -> Объект.Контрагент {OnChange, StartChoice}` — далее откроет модуль формы и найдёт процедуры по именам событий. + +### Точечное погружение в детали + +Сводка — это карта. Для деталей по конкретному элементу достаточно grep по имени из сводки: + +``` +> Покажи все свойства элемента ДоговорКонтрагента +``` + +Claude сделает grep по Form.xml и найдёт полный XML-блок с ChoiceParameters, ChoiceFoldersAndItems, стилями и прочими свойствами. + +## Чтение вывода + +### Дерево элементов + +| Обозначение | Элемент | +|---|---| +| `[Group:V]` `[Group:H]` `[Group:AH]` `[Group:AV]` | Группа (Vertical / Horizontal / AlwaysHorizontal / AlwaysVertical) | +| `[Input]` | Поле ввода | +| `[Check]` | Флажок | +| `[Label]` | Декоративная надпись | +| `[LabelField]` | Поле надписи (привязанное к данным) | +| `[Table]` | Таблица | +| `[Pages]` / `[Page]` | Вкладки (страницы показывают количество элементов) | +| `[Button]` | Кнопка | +| `[CmdBar]` | Командная панель | +| `[Popup]` | Выпадающее меню | +| `[Picture]` | Декоративная картинка | +| `[PicField]` | Поле картинки | +| `[Calendar]` | Календарь | + +### Флаги + +Показываются только при отклонении от умолчания: +- `[hidden]` — элемент скрыт (Visible=false) +- `[disabled]` — элемент недоступен (Enabled=false) +- `[ro]` — только чтение (ReadOnly=true) +- `,collapse` — сворачиваемая группа (Behavior=Collapsible) + +### Привязки и события + +- `-> Объект.Поле` — DataPath (привязка к данным) +- `-> Form.Command.Имя` — привязка к команде формы +- `{OnChange, StartChoice}` — имена подключённых событий +- `[title:Текст]` — заголовок, если отличается от имени элемента + +### Реквизиты + +- `*Объект: DocumentObject.Реализация (main)` — основной реквизит формы +- `Таблица: ValueTable [Кол1: тип, Кол2: тип]` — таблица значений с колонками +- `Список: DynamicList -> Catalog.Пользователи` — динамический список с основной таблицей + +## Примеры слеш-команд + +``` +> /form-info upload/acc_8.3.24/Documents/РеализацияТоваровУслуг/Forms/ФормаДокумента/Ext/Form.xml +> /form-info src/МояОбработка/Forms/Форма/Ext/Form.xml +``` + +## Связь с EPF-навыками + +`/form-info` работает с формами из любых источников — конфигурации и внешних обработок. При работе с обработками: + +1. `/epf-add-form` — создать форму +2. `/form-info` — проанализировать существующую форму +3. Модифицировать Form.xml, ориентируясь на сводку +4. `/epf-build` — собрать EPF + +## Спецификации + +- [Управляемая форма](1c-form-spec.md) — Form.xml, элементы, команды, реквизиты, система типов From cc2595b57a0ecd1dc5e6313e291687e98e094a20 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Mon, 9 Feb 2026 11:55:57 +0300 Subject: [PATCH 02/13] Add form-compile skill for generating Form.xml from JSON DSL Co-Authored-By: Claude Opus 4.6 --- .claude/skills/form-compile/SKILL.md | 121 ++ .../form-compile/scripts/form-compile.ps1 | 1052 +++++++++++++++++ README.md | 7 +- docs/form-dsl-spec.md | 462 ++++++++ docs/form-guide.md | 57 +- 5 files changed, 1692 insertions(+), 7 deletions(-) create mode 100644 .claude/skills/form-compile/SKILL.md create mode 100644 .claude/skills/form-compile/scripts/form-compile.ps1 create mode 100644 docs/form-dsl-spec.md diff --git a/.claude/skills/form-compile/SKILL.md b/.claude/skills/form-compile/SKILL.md new file mode 100644 index 00000000..90b9b59c --- /dev/null +++ b/.claude/skills/form-compile/SKILL.md @@ -0,0 +1,121 @@ +--- +name: form-compile +description: Компиляция управляемой формы 1С (Form.xml) из компактного JSON-определения +argument-hint: +allowed-tools: + - Bash + - Read + - Write + - Glob +--- + +# /form-compile — Генерация Form.xml из JSON DSL + +Принимает компактное JSON-определение формы (20–50 строк) и генерирует полный корректный Form.xml (100–500+ строк) с namespace-декларациями, автогенерированными companion-элементами, последовательными ID. + +## Использование + +``` +/form-compile +``` + +## Параметры + +| Параметр | Обязательный | Описание | +|------------|:------------:|-----------------------------------| +| JsonPath | да | Путь к JSON-определению формы | +| OutputPath | да | Путь к выходному файлу Form.xml | + +## Команда + +```powershell +powershell.exe -NoProfile -File .claude\skills\form-compile\scripts\form-compile.ps1 -JsonPath "" -OutputPath "" +``` + +## JSON DSL — краткая справка + +### Элементы (ключ определяет тип) + +| DSL ключ | XML элемент | Значение ключа | +|--------------|-------------------|---------------------------------------------------| +| `"group"` | UsualGroup | `"horizontal"` / `"vertical"` / `"alwaysHorizontal"` / `"alwaysVertical"` / `"collapsible"` | +| `"input"` | InputField | имя элемента | +| `"check"` | CheckBoxField | имя | +| `"label"` | LabelDecoration | имя | +| `"labelField"` | LabelField | имя | +| `"table"` | Table | имя | +| `"pages"` | Pages | имя | +| `"page"` | Page | имя | +| `"button"` | Button | имя | +| `"picture"` | PictureDecoration | имя | +| `"picField"` | PictureField | имя | +| `"calendar"` | CalendarField | имя | +| `"cmdBar"` | CommandBar | имя | +| `"popup"` | Popup | имя | + +### Общие свойства элементов + +- `"name"` — переопределить имя (по умолчанию из значения ключа типа) +- `"path"` — DataPath (привязка к данным) +- `"title"` — заголовок +- `"hidden": true` — Visible=false +- `"disabled": true` — Enabled=false +- `"readOnly": true` — ReadOnly=true +- `"on": ["OnChange", "StartChoice"]` — события с автоименованием обработчиков + +### Система типов (shorthand) + +| DSL | XML | +|------------------------|----------------------------------------| +| `"string"` / `"string(100)"` | `xs:string` + StringQualifiers | +| `"decimal(15,2)"` | `xs:decimal` + NumberQualifiers | +| `"decimal(10,0,nonneg)"` | с AllowedSign=Nonnegative | +| `"boolean"` | `xs:boolean` | +| `"date"` / `"dateTime"` / `"time"` | `xs:dateTime` + DateFractions | +| `"CatalogRef.Организации"` | `cfg:CatalogRef.Организации` | +| `"DocumentObject.Реализация"` | `cfg:DocumentObject.Реализация` | +| `"ValueTable"` | `v8:ValueTable` | +| `"ValueList"` | `v8:ValueListType` | +| `"Type1 \| Type2"` | составной тип | + +### Пример JSON + +```json +{ + "title": "Загрузка данных", + "properties": { "autoTitle": false }, + "events": { "OnCreateAtServer": "ПриСозданииНаСервере" }, + "elements": [ + { "group": "horizontal", "name": "Шапка", "children": [ + { "input": "Организация", "path": "Объект.Организация", "on": ["OnChange"] } + ]}, + { "table": "Товары", "path": "Объект.Товары", "columns": [ + { "input": "Номенклатура", "path": "Объект.Товары.Номенклатура" } + ]} + ], + "attributes": [ + { "name": "Объект", "type": "DataProcessorObject.ЗагрузкаДанных", "main": true } + ], + "commands": [ + { "name": "Загрузить", "action": "ЗагрузитьОбработка", "shortcut": "Ctrl+Enter" } + ] +} +``` + +### Автогенерация + +- **Companion-элементы**: ContextMenu, ExtendedTooltip и др. создаются автоматически с правильными именами и ID +- **Обработчики событий**: `"on": ["OnChange"]` → `ОрганизацияПриИзменении` +- **Namespace**: все 17 namespace-деклараций добавляются автоматически +- **ID**: последовательная нумерация, AutoCommandBar всегда id="-1" + +## Верификация + +Используйте `/form-info` для проверки результата: + +``` +/form-info +``` + +Структура в сводке должна совпадать с определением в JSON. + diff --git a/.claude/skills/form-compile/scripts/form-compile.ps1 b/.claude/skills/form-compile/scripts/form-compile.ps1 new file mode 100644 index 00000000..c10d1acf --- /dev/null +++ b/.claude/skills/form-compile/scripts/form-compile.ps1 @@ -0,0 +1,1052 @@ +param( + [Parameter(Mandatory)] + [string]$JsonPath, + + [Parameter(Mandatory)] + [string]$OutputPath +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- 1. Load and validate JSON --- + +if (-not (Test-Path $JsonPath)) { + Write-Error "File not found: $JsonPath" + exit 1 +} + +$json = Get-Content -Raw -Encoding UTF8 $JsonPath +$def = $json | ConvertFrom-Json + +# --- 2. ID allocator --- + +$script:nextId = 1 +function New-Id { + $id = $script:nextId + $script:nextId++ + return $id +} + +# --- 3. XML helper --- + +$script:xml = New-Object System.Text.StringBuilder 8192 + +function X { + param([string]$text) + $script:xml.AppendLine($text) | Out-Null +} + +function Esc-Xml { + param([string]$s) + return $s.Replace('&','&').Replace('<','<').Replace('>','>').Replace('"','"') +} + +# --- 4. Multilang helper --- + +function Emit-MLText { + param([string]$tag, [string]$text, [string]$indent) + X "$indent<$tag>" + X "$indent`t" + X "$indent`t`tru" + X "$indent`t`t$(Esc-Xml $text)" + X "$indent`t" + X "$indent" +} + +# --- 5. Type emitter --- + +function Emit-Type { + param($typeStr, [string]$indent) + + if (-not $typeStr) { + X "$indent" + return + } + + $typeString = "$typeStr" + + # Composite type: "Type1 | Type2" + $parts = $typeString -split '\s*\|\s*' + + X "$indent" + foreach ($part in $parts) { + $part = $part.Trim() + Emit-SingleType -typeStr $part -indent "$indent`t" + } + X "$indent" +} + +function Emit-SingleType { + param([string]$typeStr, [string]$indent) + + # boolean + if ($typeStr -eq "boolean") { + X "$indentxs:boolean" + return + } + + # string or string(N) + if ($typeStr -match '^string(\((\d+)\))?$') { + $len = if ($Matches[2]) { $Matches[2] } else { "0" } + X "$indentxs:string" + X "$indent" + X "$indent`t$len" + X "$indent`tVariable" + X "$indent" + return + } + + # decimal(D,F) or decimal(D,F,nonneg) + if ($typeStr -match '^decimal\((\d+),(\d+)(,nonneg)?\)$') { + $digits = $Matches[1] + $fraction = $Matches[2] + $sign = if ($Matches[3]) { "Nonnegative" } else { "Any" } + X "$indentxs:decimal" + X "$indent" + X "$indent`t$digits" + X "$indent`t$fraction" + X "$indent`t$sign" + X "$indent" + return + } + + # date / dateTime / time + if ($typeStr -match '^(date|dateTime|time)$') { + $fractions = switch ($typeStr) { + "date" { "Date" } + "dateTime" { "DateTime" } + "time" { "Time" } + } + X "$indentxs:dateTime" + X "$indent" + X "$indent`t$fractions" + X "$indent" + return + } + + # ValueTable, ValueTree, ValueList, etc. + $v8Types = @{ + "ValueTable" = "v8:ValueTable" + "ValueTree" = "v8:ValueTree" + "ValueList" = "v8:ValueListType" + "TypeDescription" = "v8:TypeDescription" + "Universal" = "v8:Universal" + "FixedArray" = "v8:FixedArray" + "FixedStructure" = "v8:FixedStructure" + } + if ($v8Types.ContainsKey($typeStr)) { + X "$indent$($v8Types[$typeStr])" + return + } + + # UI types + $uiTypes = @{ + "FormattedString" = "v8ui:FormattedString" + "Picture" = "v8ui:Picture" + "Color" = "v8ui:Color" + "Font" = "v8ui:Font" + } + if ($uiTypes.ContainsKey($typeStr)) { + X "$indent$($uiTypes[$typeStr])" + return + } + + # DCS types + if ($typeStr -match '^DataComposition') { + $dcsMap = @{ + "DataCompositionSettings" = "dcsset:DataCompositionSettings" + "DataCompositionSchema" = "dcssch:DataCompositionSchema" + "DataCompositionComparisonType" = "dcscor:DataCompositionComparisonType" + } + if ($dcsMap.ContainsKey($typeStr)) { + X "$indent$($dcsMap[$typeStr])" + return + } + } + + # DynamicList + if ($typeStr -eq "DynamicList") { + X "$indentcfg:DynamicList" + return + } + + # cfg: references (CatalogRef.XXX, DocumentObject.XXX, etc.) + if ($typeStr -match '^(CatalogRef|CatalogObject|DocumentRef|DocumentObject|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|TaskRef|InformationRegisterRecordSet|AccumulationRegisterRecordSet|DataProcessorObject)\.') { + X "$indentcfg:$typeStr" + return + } + + # Fallback: emit as-is with cfg: prefix if contains dot, otherwise v8: + if ($typeStr.Contains('.')) { + X "$indentcfg:$typeStr" + } else { + X "$indent$typeStr" + } +} + +# --- 6. Event handler name generator --- + +$script:eventSuffixMap = @{ + "OnChange" = "ПриИзменении" + "StartChoice" = "НачалоВыбора" + "ChoiceProcessing" = "ОбработкаВыбора" + "AutoComplete" = "АвтоПодбор" + "Clearing" = "Очистка" + "Opening" = "Открытие" + "Click" = "Нажатие" + "OnActivateRow" = "ПриАктивизацииСтроки" + "BeforeAddRow" = "ПередНачаломДобавления" + "BeforeDeleteRow" = "ПередУдалением" + "BeforeRowChange" = "ПередНачаломИзменения" + "OnStartEdit" = "ПриНачалеРедактирования" + "OnEndEdit" = "ПриОкончанииРедактирования" + "Selection" = "ВыборСтроки" + "OnCurrentPageChange" = "ПриСменеСтраницы" + "TextEditEnd" = "ОкончаниеВводаТекста" + "URLProcessing" = "ОбработкаНавигационнойСсылки" + "DragStart" = "НачалоПеретаскивания" + "Drag" = "Перетаскивание" + "DragCheck" = "ПроверкаПеретаскивания" + "Drop" = "Помещение" + "AfterDeleteRow" = "ПослеУдаления" +} + +function Get-HandlerName { + param([string]$elementName, [string]$eventName) + $suffix = $script:eventSuffixMap[$eventName] + if ($suffix) { + return "$elementName$suffix" + } + return "$elementName$eventName" +} + +# --- 7. Element emitters --- + +function Get-ElementName { + param($el, [string]$typeKey) + if ($el.name) { return "$($el.name)" } + return "$($el.$typeKey)" +} + +function Emit-Events { + param($el, [string]$elementName, [string]$indent) + + if (-not $el.on) { return } + + X "$indent" + foreach ($evt in $el.on) { + $evtName = "$evt" + $handler = if ($el.handlers -and $el.handlers.$evtName) { + "$($el.handlers.$evtName)" + } else { + Get-HandlerName -elementName $elementName -eventName $evtName + } + X "$indent`t$handler" + } + X "$indent" +} + +function Emit-Companion { + param([string]$tag, [string]$name, [string]$indent) + $id = New-Id + X "$indent<$tag name=`"$name`" id=`"$id`"/>" +} + +function Emit-Element { + param($el, [string]$indent) + + # Determine element type from key + $typeKey = $null + $xmlTag = $null + + foreach ($key in @("group","input","check","label","labelField","table","pages","page","button","picture","picField","calendar","cmdBar","popup")) { + if ($el.$key -ne $null) { + $typeKey = $key + break + } + } + + if (-not $typeKey) { + Write-Warning "Unknown element type, skipping" + return + } + + $name = Get-ElementName -el $el -typeKey $typeKey + $id = New-Id + + switch ($typeKey) { + "group" { Emit-Group -el $el -name $name -id $id -indent $indent } + "input" { Emit-Input -el $el -name $name -id $id -indent $indent } + "check" { Emit-Check -el $el -name $name -id $id -indent $indent } + "label" { Emit-Label -el $el -name $name -id $id -indent $indent } + "labelField" { Emit-LabelField -el $el -name $name -id $id -indent $indent } + "table" { Emit-Table -el $el -name $name -id $id -indent $indent } + "pages" { Emit-Pages -el $el -name $name -id $id -indent $indent } + "page" { Emit-Page -el $el -name $name -id $id -indent $indent } + "button" { Emit-Button -el $el -name $name -id $id -indent $indent } + "picture" { Emit-PictureDecoration -el $el -name $name -id $id -indent $indent } + "picField" { Emit-PictureField -el $el -name $name -id $id -indent $indent } + "calendar" { Emit-Calendar -el $el -name $name -id $id -indent $indent } + "cmdBar" { Emit-CommandBar -el $el -name $name -id $id -indent $indent } + "popup" { Emit-Popup -el $el -name $name -id $id -indent $indent } + } +} + +function Emit-CommonFlags { + param($el, [string]$indent) + if ($el.hidden -eq $true) { X "$indentfalse" } + if ($el.disabled -eq $true) { X "$indentfalse" } + if ($el.readOnly -eq $true) { X "$indenttrue" } +} + +function Emit-Title { + param($el, [string]$name, [string]$indent) + if ($el.title) { + Emit-MLText -tag "Title" -text "$($el.title)" -indent $indent + } +} + +function Emit-Group { + param($el, [string]$name, [int]$id, [string]$indent) + + X "$indent" + $inner = "$indent`t" + + Emit-Title -el $el -name $name -indent $inner + + # Group orientation + $groupVal = "$($el.group)" + $orientation = switch ($groupVal) { + "horizontal" { "Horizontal" } + "vertical" { "Vertical" } + "alwaysHorizontal" { "AlwaysHorizontal" } + "alwaysVertical" { "AlwaysVertical" } + default { $null } + } + if ($orientation) { X "$inner$orientation" } + + # Behavior + if ($groupVal -eq "collapsible") { + X "$innerVertical" + X "$innerCollapsible" + } + + # Representation + if ($el.representation) { + $repr = switch ("$($el.representation)") { + "none" { "None" } + "normal" { "NormalSeparation" } + "weak" { "WeakSeparation" } + "strong" { "StrongSeparation" } + default { "$($el.representation)" } + } + X "$inner$repr" + } + + # ShowTitle + if ($el.showTitle -eq $false) { X "$innerfalse" } + + # United + if ($el.united -eq $false) { X "$innerfalse" } + + Emit-CommonFlags -el $el -indent $inner + + # Companion: ExtendedTooltip + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner + + # Children + if ($el.children -and $el.children.Count -gt 0) { + X "$inner" + foreach ($child in $el.children) { + Emit-Element -el $child -indent "$inner`t" + } + X "$inner" + } + + X "$indent" +} + +function Emit-Input { + param($el, [string]$name, [int]$id, [string]$indent) + + X "$indent" + $inner = "$indent`t" + + if ($el.path) { X "$inner$($el.path)" } + + Emit-Title -el $el -name $name -indent $inner + Emit-CommonFlags -el $el -indent $inner + + if ($el.titleLocation) { + $loc = switch ("$($el.titleLocation)") { + "none" { "None" } + "left" { "Left" } + "right" { "Right" } + "top" { "Top" } + "bottom" { "Bottom" } + default { "$($el.titleLocation)" } + } + X "$inner$loc" + } + + if ($el.multiLine -eq $true) { X "$innertrue" } + if ($el.passwordMode -eq $true) { X "$innertrue" } + if ($el.choiceButton -eq $false) { X "$innerfalse" } + if ($el.clearButton -eq $true) { X "$innertrue" } + if ($el.spinButton -eq $true) { X "$innertrue" } + if ($el.dropListButton -eq $true) { X "$innertrue" } + if ($el.markIncomplete -eq $true) { X "$innertrue" } + if ($el.skipOnInput -eq $true) { X "$innertrue" } + if ($el.autoMaxWidth -eq $false) { X "$innerfalse" } + if ($el.autoMaxHeight -eq $false) { X "$innerfalse" } + if ($el.width) { X "$inner$($el.width)" } + if ($el.height) { X "$inner$($el.height)" } + if ($el.horizontalStretch -eq $true) { X "$innertrue" } + if ($el.verticalStretch -eq $true) { X "$innertrue" } + + if ($el.inputHint) { + Emit-MLText -tag "InputHint" -text "$($el.inputHint)" -indent $inner + } + + # Companions + Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner + + Emit-Events -el $el -elementName $name -indent $inner + + X "$indent" +} + +function Emit-Check { + param($el, [string]$name, [int]$id, [string]$indent) + + X "$indent" + $inner = "$indent`t" + + if ($el.path) { X "$inner$($el.path)" } + + Emit-Title -el $el -name $name -indent $inner + Emit-CommonFlags -el $el -indent $inner + + if ($el.titleLocation) { + X "$inner$($el.titleLocation)" + } + + # Companions + Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner + + Emit-Events -el $el -elementName $name -indent $inner + + X "$indent" +} + +function Emit-Label { + param($el, [string]$name, [int]$id, [string]$indent) + + X "$indent" + $inner = "$indent`t" + + if ($el.title) { + $formatted = if ($el.hyperlink -eq $true) { "true" } else { "false" } + X "$inner" + X "$inner`t<v8:item>" + X "$inner`t`t<v8:lang>ru</v8:lang>" + X "$inner`t`t<v8:content>$(Esc-Xml "$($el.title)")</v8:content>" + X "$inner`t</v8:item>" + X "$inner" + } + + Emit-CommonFlags -el $el -indent $inner + + if ($el.hyperlink -eq $true) { X "$innertrue" } + if ($el.autoMaxWidth -eq $false) { X "$innerfalse" } + if ($el.autoMaxHeight -eq $false) { X "$innerfalse" } + if ($el.width) { X "$inner$($el.width)" } + if ($el.height) { X "$inner$($el.height)" } + + # Companions + Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner + + Emit-Events -el $el -elementName $name -indent $inner + + X "$indent" +} + +function Emit-LabelField { + param($el, [string]$name, [int]$id, [string]$indent) + + X "$indent" + $inner = "$indent`t" + + if ($el.path) { X "$inner$($el.path)" } + + Emit-Title -el $el -name $name -indent $inner + Emit-CommonFlags -el $el -indent $inner + + if ($el.hyperlink -eq $true) { X "$innertrue" } + + # Companions + Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner + + Emit-Events -el $el -elementName $name -indent $inner + + X "$indent" +} + +function Emit-Table { + param($el, [string]$name, [int]$id, [string]$indent) + + X "$indent" + $inner = "$indent`t" + + if ($el.path) { X "$inner$($el.path)" } + + Emit-Title -el $el -name $name -indent $inner + Emit-CommonFlags -el $el -indent $inner + + if ($el.representation) { + X "$inner$($el.representation)" + } + if ($el.changeRowSet -eq $true) { X "$innertrue" } + if ($el.changeRowOrder -eq $true) { X "$innertrue" } + if ($el.height) { X "$inner$($el.height)" } + if ($el.header -eq $false) { X "$inner
false
" } + if ($el.footer -eq $true) { X "$inner
true
" } + + if ($el.commandBarLocation) { + X "$inner$($el.commandBarLocation)" + } + if ($el.searchStringLocation) { + X "$inner$($el.searchStringLocation)" + } + + # Companions + Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner + Emit-Companion -tag "AutoCommandBar" -name "${name}КоманднаяПанель" -indent $inner + Emit-Companion -tag "SearchStringAddition" -name "${name}СтрокаПоиска" -indent $inner + Emit-Companion -tag "ViewStatusAddition" -name "${name}СостояниеПросмотра" -indent $inner + Emit-Companion -tag "SearchControlAddition" -name "${name}УправлениеПоиском" -indent $inner + + # Columns + if ($el.columns -and $el.columns.Count -gt 0) { + X "$inner" + foreach ($col in $el.columns) { + Emit-Element -el $col -indent "$inner`t" + } + X "$inner" + } + + Emit-Events -el $el -elementName $name -indent $inner + + X "$indent
" +} + +function Emit-Pages { + param($el, [string]$name, [int]$id, [string]$indent) + + X "$indent" + $inner = "$indent`t" + + if ($el.pagesRepresentation) { + X "$inner$($el.pagesRepresentation)" + } + + Emit-CommonFlags -el $el -indent $inner + + # Companion + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner + + Emit-Events -el $el -elementName $name -indent $inner + + # Children (pages) + if ($el.children -and $el.children.Count -gt 0) { + X "$inner" + foreach ($child in $el.children) { + Emit-Element -el $child -indent "$inner`t" + } + X "$inner" + } + + X "$indent" +} + +function Emit-Page { + param($el, [string]$name, [int]$id, [string]$indent) + + X "$indent" + $inner = "$indent`t" + + Emit-Title -el $el -name $name -indent $inner + Emit-CommonFlags -el $el -indent $inner + + if ($el.group) { + $orientation = switch ("$($el.group)") { + "horizontal" { "Horizontal" } + "vertical" { "Vertical" } + "alwaysHorizontal" { "AlwaysHorizontal" } + "alwaysVertical" { "AlwaysVertical" } + default { $null } + } + if ($orientation) { X "$inner$orientation" } + } + + # Companion + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner + + # Children + if ($el.children -and $el.children.Count -gt 0) { + X "$inner" + foreach ($child in $el.children) { + Emit-Element -el $child -indent "$inner`t" + } + X "$inner" + } + + X "$indent" +} + +function Emit-Button { + param($el, [string]$name, [int]$id, [string]$indent) + + X "$indent" +} + +function Emit-PictureDecoration { + param($el, [string]$name, [int]$id, [string]$indent) + + X "$indent" + $inner = "$indent`t" + + Emit-Title -el $el -name $name -indent $inner + Emit-CommonFlags -el $el -indent $inner + + if ($el.picture -or $el.src) { + $ref = if ($el.src) { "$($el.src)" } else { "$($el.picture)" } + X "$inner" + X "$inner`t$ref" + X "$inner`ttrue" + X "$inner" + } + + if ($el.hyperlink -eq $true) { X "$innertrue" } + if ($el.width) { X "$inner$($el.width)" } + if ($el.height) { X "$inner$($el.height)" } + + # Companions + Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner + + Emit-Events -el $el -elementName $name -indent $inner + + X "$indent" +} + +function Emit-PictureField { + param($el, [string]$name, [int]$id, [string]$indent) + + X "$indent" + $inner = "$indent`t" + + if ($el.path) { X "$inner$($el.path)" } + + Emit-Title -el $el -name $name -indent $inner + Emit-CommonFlags -el $el -indent $inner + + if ($el.width) { X "$inner$($el.width)" } + if ($el.height) { X "$inner$($el.height)" } + + # Companions + Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner + + Emit-Events -el $el -elementName $name -indent $inner + + X "$indent" +} + +function Emit-Calendar { + param($el, [string]$name, [int]$id, [string]$indent) + + X "$indent" + $inner = "$indent`t" + + if ($el.path) { X "$inner$($el.path)" } + + Emit-Title -el $el -name $name -indent $inner + Emit-CommonFlags -el $el -indent $inner + + # Companions + Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner + + Emit-Events -el $el -elementName $name -indent $inner + + X "$indent" +} + +function Emit-CommandBar { + param($el, [string]$name, [int]$id, [string]$indent) + + X "$indent" + $inner = "$indent`t" + + if ($el.autofill -eq $true) { X "$innertrue" } + + Emit-CommonFlags -el $el -indent $inner + + # Children + if ($el.children -and $el.children.Count -gt 0) { + X "$inner" + foreach ($child in $el.children) { + Emit-Element -el $child -indent "$inner`t" + } + X "$inner" + } + + X "$indent" +} + +function Emit-Popup { + param($el, [string]$name, [int]$id, [string]$indent) + + X "$indent" + $inner = "$indent`t" + + Emit-Title -el $el -name $name -indent $inner + Emit-CommonFlags -el $el -indent $inner + + if ($el.picture) { + X "$inner" + X "$inner`t$($el.picture)" + X "$inner`ttrue" + X "$inner" + } + + if ($el.representation) { + X "$inner$($el.representation)" + } + + # Children + if ($el.children -and $el.children.Count -gt 0) { + X "$inner" + foreach ($child in $el.children) { + Emit-Element -el $child -indent "$inner`t" + } + X "$inner" + } + + X "$indent" +} + +# --- 8. Attribute emitter --- + +function Emit-Attributes { + param($attrs, [string]$indent) + + if (-not $attrs -or $attrs.Count -eq 0) { return } + + X "$indent" + foreach ($attr in $attrs) { + $attrId = New-Id + $attrName = "$($attr.name)" + + X "$indent`t" + $inner = "$indent`t`t" + + if ($attr.title) { + Emit-MLText -tag "Title" -text "$($attr.title)" -indent $inner + } + + # Type + if ($attr.type) { + Emit-Type -typeStr "$($attr.type)" -indent $inner + } else { + X "$inner" + } + + if ($attr.main -eq $true) { + X "$innertrue" + } + if ($attr.savedData -eq $true) { + X "$innertrue" + } + if ($attr.fillChecking) { + X "$inner$($attr.fillChecking)" + } + + # Columns (for ValueTable/ValueTree) + if ($attr.columns -and $attr.columns.Count -gt 0) { + X "$inner" + foreach ($col in $attr.columns) { + $colId = New-Id + X "$inner`t" + if ($col.title) { + Emit-MLText -tag "Title" -text "$($col.title)" -indent "$inner`t`t" + } + Emit-Type -typeStr "$($col.type)" -indent "$inner`t`t" + X "$inner`t" + } + X "$inner" + } + + X "$indent`t" + } + X "$indent" +} + +# --- 9. Parameter emitter --- + +function Emit-Parameters { + param($params, [string]$indent) + + if (-not $params -or $params.Count -eq 0) { return } + + X "$indent" + foreach ($param in $params) { + X "$indent`t" + $inner = "$indent`t`t" + + Emit-Type -typeStr "$($param.type)" -indent $inner + + if ($param.key -eq $true) { + X "$innertrue" + } + + X "$indent`t" + } + X "$indent" +} + +# --- 10. Command emitter --- + +function Emit-Commands { + param($cmds, [string]$indent) + + if (-not $cmds -or $cmds.Count -eq 0) { return } + + X "$indent" + foreach ($cmd in $cmds) { + $cmdId = New-Id + X "$indent`t" + $inner = "$indent`t`t" + + if ($cmd.title) { + Emit-MLText -tag "Title" -text "$($cmd.title)" -indent $inner + } + + if ($cmd.action) { + X "$inner$($cmd.action)" + } + + if ($cmd.shortcut) { + X "$inner$($cmd.shortcut)" + } + + if ($cmd.picture) { + X "$inner" + X "$inner`t$($cmd.picture)" + X "$inner`ttrue" + X "$inner" + } + + if ($cmd.representation) { + X "$inner$($cmd.representation)" + } + + X "$indent`t" + } + X "$indent" +} + +# --- 11. Properties emitter --- + +function Emit-Properties { + param($props, [string]$indent) + + if (-not $props) { return } + + # camelCase -> PascalCase mapping for known properties + $propMap = @{ + "autoTitle" = "AutoTitle" + "windowOpeningMode" = "WindowOpeningMode" + "commandBarLocation" = "CommandBarLocation" + "saveDataInSettings" = "SaveDataInSettings" + "autoSaveDataInSettings" = "AutoSaveDataInSettings" + "autoTime" = "AutoTime" + "usePostingMode" = "UsePostingMode" + "repostOnWrite" = "RepostOnWrite" + "autoURL" = "AutoURL" + "autoFillCheck" = "AutoFillCheck" + "customizable" = "Customizable" + "enterKeyBehavior" = "EnterKeyBehavior" + "verticalScroll" = "VerticalScroll" + "scalingMode" = "ScalingMode" + "useForFoldersAndItems" = "UseForFoldersAndItems" + "reportResult" = "ReportResult" + "detailsData" = "DetailsData" + "reportFormType" = "ReportFormType" + "autoShowState" = "AutoShowState" + "width" = "Width" + "height" = "Height" + "group" = "Group" + } + + foreach ($p in $props.PSObject.Properties) { + $xmlName = if ($propMap.ContainsKey($p.Name)) { $propMap[$p.Name] } else { + # Auto PascalCase: first letter uppercase + $p.Name.Substring(0,1).ToUpper() + $p.Name.Substring(1) + } + # Convert boolean to lowercase string (PS renders as True/False) + $val = $p.Value + if ($val -is [bool]) { + $val = if ($val) { "true" } else { "false" } + } + X "$indent<$xmlName>$val" + } +} + +# --- 12. Main compilation --- + +# Title +if ($def.title) { + Emit-MLText -tag "Title" -text "$($def.title)" -indent "`t" +} + +# Header +X '' +X '
' + +# Oops — Title was emitted before header. Need to fix the order. +# Actually, let me restructure: build the body into a separate buffer, then assemble + +# Reset and rebuild properly +$script:xml = New-Object System.Text.StringBuilder 8192 +$script:nextId = 1 + +X '' +X '' + +# 12a. Title +if ($def.title) { + Emit-MLText -tag "Title" -text "$($def.title)" -indent "`t" +} + +# 12b. Properties +Emit-Properties -props $def.properties -indent "`t" + +# 12c. CommandSet (excluded commands) +if ($def.excludedCommands -and $def.excludedCommands.Count -gt 0) { + X "`t" + foreach ($cmd in $def.excludedCommands) { + X "`t`t$cmd" + } + X "`t" +} + +# 12d. AutoCommandBar (always present, id=-1) +X "`t" +X "`t`tRight" +X "`t`tfalse" +X "`t" + +# 12e. Events +if ($def.events) { + X "`t" + foreach ($p in $def.events.PSObject.Properties) { + X "`t`t$($p.Value)" + } + X "`t" +} + +# 12f. ChildItems (elements) +if ($def.elements -and $def.elements.Count -gt 0) { + X "`t" + foreach ($el in $def.elements) { + Emit-Element -el $el -indent "`t`t" + } + X "`t" +} + +# 12g. Attributes +Emit-Attributes -attrs $def.attributes -indent "`t" + +# 12h. Parameters +Emit-Parameters -params $def.parameters -indent "`t" + +# 12i. Commands +Emit-Commands -cmds $def.commands -indent "`t" + +# 12j. Close +X '
' + +# --- 13. Write output --- + +$outPath = if ([System.IO.Path]::IsPathRooted($OutputPath)) { $OutputPath } else { Join-Path (Get-Location) $OutputPath } +$outDir = [System.IO.Path]::GetDirectoryName($outPath) +if (-not (Test-Path $outDir)) { + New-Item -ItemType Directory -Path $outDir -Force | Out-Null +} + +$enc = New-Object System.Text.UTF8Encoding($true) +[System.IO.File]::WriteAllText($outPath, $xml.ToString(), $enc) + +# --- 14. Summary --- + +$elCount = $script:nextId - 1 +Write-Host "[OK] Compiled: $OutputPath" +Write-Host " Elements+IDs: $elCount" +if ($def.attributes) { Write-Host " Attributes: $($def.attributes.Count)" } +if ($def.commands) { Write-Host " Commands: $($def.commands.Count)" } +if ($def.parameters) { Write-Host " Parameters: $($def.parameters.Count)" } diff --git a/README.md b/README.md index 3e402407..087724ca 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ |--------|--------|----------|------| | Внешние обработки (EPF) | 10 навыков `/epf-*` | Создание, модификация, сборка обработок из XML-исходников | [Подробнее](docs/epf-guide.md) | | Табличный документ (MXL) | 4 навыка `/mxl-*` | Анализ, создание, компиляция макетов печатных форм | [Подробнее](docs/mxl-guide.md) | -| Управляемые формы (Form) | `/form-info` | Анализ структуры управляемых форм из XML-исходников конфигурации | [Подробнее](docs/form-guide.md) | +| Управляемые формы (Form) | 2 навыка `/form-*` | Анализ и генерация управляемых форм из XML-исходников | [Подробнее](docs/form-guide.md) | | Утилиты | `/img-grid` | Наложение сетки на изображение для определения пропорций колонок | — | ## Требования @@ -37,6 +37,7 @@ - [Сборка и разборка EPF](docs/build-spec.md) — команды `1cv8.exe`, параметры, коды возврата - [Табличный документ (MXL)](docs/1c-spreadsheet-spec.md) — XML-формат SpreadsheetDocument, совместимость версий - [MXL DSL](docs/mxl-dsl-spec.md) — JSON-формат описания макета для `/mxl-compile` и `/mxl-decompile` +- [Form DSL](docs/form-dsl-spec.md) — JSON-формат описания формы для `/form-compile` ## Структура репозитория @@ -57,6 +58,7 @@ ├── mxl-compile/ # Компиляция макета из JSON ├── mxl-decompile/ # Декомпиляция макета в JSON ├── form-info/ # Анализ структуры управляемой формы +├── form-compile/ # Компиляция формы из JSON └── img-grid/ # Сетка для анализа изображений docs/ ├── epf-guide.md # Гайд: внешние обработки @@ -67,5 +69,6 @@ docs/ ├── 1c-help-spec.md # Спецификация встроенной справки ├── build-spec.md # Спецификация сборки/разборки ├── 1c-spreadsheet-spec.md # Спецификация табличного документа -└── mxl-dsl-spec.md # Спецификация MXL DSL +├── mxl-dsl-spec.md # Спецификация MXL DSL +└── form-dsl-spec.md # Спецификация Form DSL ``` diff --git a/docs/form-dsl-spec.md b/docs/form-dsl-spec.md new file mode 100644 index 00000000..e847354d --- /dev/null +++ b/docs/form-dsl-spec.md @@ -0,0 +1,462 @@ +# Form DSL Specification + +Спецификация JSON-формата для `/form-compile` — компактного описания управляемых форм 1С:Предприятия 8.3. + +--- + +## 1. Корневой объект + +```json +{ + "title": "Заголовок формы", + "properties": { ... }, + "excludedCommands": [ ... ], + "events": { ... }, + "elements": [ ... ], + "attributes": [ ... ], + "parameters": [ ... ], + "commands": [ ... ] +} +``` + +| Поле | Тип | Описание | +|------|-----|----------| +| `title` | string | Заголовок формы (необязательный) | +| `properties` | object | Свойства формы (необязательный) | +| `excludedCommands` | string[] | Исключённые стандартные команды (необязательный) | +| `events` | object | Обработчики событий формы (необязательный) | +| `elements` | array | Дерево UI-элементов (необязательный) | +| `attributes` | array | Реквизиты формы (необязательный) | +| `parameters` | array | Параметры формы (необязательный) | +| `commands` | array | Команды формы (необязательный) | + +--- + +## 2. Properties — свойства формы + +Объект со свойствами в camelCase. Компилятор преобразует в PascalCase для XML. + +```json +"properties": { + "autoTitle": false, + "windowOpeningMode": "LockOwnerWindow", + "commandBarLocation": "Bottom" +} +``` + +### Поддерживаемые свойства + +| DSL ключ | XML элемент | Значения | +|----------|-------------|----------| +| `autoTitle` | `` | `true` / `false` | +| `windowOpeningMode` | `` | `LockOwnerWindow`, `Modeless` | +| `commandBarLocation` | `` | `Top`, `Bottom`, `None` | +| `saveDataInSettings` | `` | `UseList`, `Use`, `DontUse` | +| `autoSaveDataInSettings` | `` | `Use`, `DontUse` | +| `autoTime` | `` | `CurrentOrLast`, `Current`, `Last` | +| `usePostingMode` | `` | `Auto`, `Postings`, `Movements` | +| `repostOnWrite` | `` | `true` / `false` | +| `autoURL` | `` | `true` / `false` | +| `autoFillCheck` | `` | `true` / `false` | +| `customizable` | `` | `true` / `false` | +| `enterKeyBehavior` | `` | `DefaultButton`, `NewLine` | +| `verticalScroll` | `` | `useIfNecessary`, `Auto`, `AlwaysShow`, `Never` | +| `width` | `` | число | +| `height` | `` | число | +| `group` | `` | `Vertical`, `Horizontal`, `AlwaysHorizontal`, `AlwaysVertical` | +| `useForFoldersAndItems` | `` | `Folders`, `Items`, `FoldersAndItems` | + +Нераспознанные ключи преобразуются с автоматическим PascalCase (первая буква в верхний регистр). + +--- + +## 3. Events — обработчики событий формы + +```json +"events": { + "OnCreateAtServer": "ПриСозданииНаСервере", + "OnOpen": "ПриОткрытии" +} +``` + +Ключ — имя события, значение — имя процедуры-обработчика. + +### Доступные события + +| Событие | Описание | +|---------|----------| +| `OnCreateAtServer` | Создание формы на сервере | +| `OnOpen` | Открытие формы | +| `BeforeClose` | Перед закрытием | +| `OnClose` | При закрытии | +| `BeforeWrite` | Перед записью | +| `BeforeWriteAtServer` | Перед записью на сервере | +| `OnWriteAtServer` | При записи на сервере | +| `AfterWriteAtServer` | После записи на сервере | +| `AfterWrite` | После записи | +| `OnReadAtServer` | При чтении объекта | +| `NotificationProcessing` | Обработка оповещений | +| `ChoiceProcessing` | Обработка выбора | +| `FillCheckProcessingAtServer` | Проверка заполнения | + +--- + +## 4. Elements — дерево UI-элементов + +Массив объектов. Тип элемента определяется ключом-идентификатором. + +### 4.1. Общие свойства всех элементов + +| Свойство | Тип | Описание | +|----------|-----|----------| +| `name` | string | Имя элемента (по умолчанию — из значения ключа типа) | +| `title` | string | Заголовок | +| `hidden` | bool | `true` → `false` | +| `disabled` | bool | `true` → `false` | +| `readOnly` | bool | `true` → `true` | +| `on` | string[] | Массив имён событий | +| `handlers` | object | Явные имена обработчиков: `{"OnChange": "МойОбработчик"}` | + +### 4.2. Автоименование обработчиков + +При указании `"on"` без `"handlers"` имя обработчика генерируется автоматически: + +``` +<ИмяЭлемента><РусскийСуффикс> +``` + +| Событие | Суффикс | +|---------|---------| +| `OnChange` | `ПриИзменении` | +| `StartChoice` | `НачалоВыбора` | +| `ChoiceProcessing` | `ОбработкаВыбора` | +| `AutoComplete` | `АвтоПодбор` | +| `Clearing` | `Очистка` | +| `Opening` | `Открытие` | +| `Click` | `Нажатие` | +| `OnActivateRow` | `ПриАктивизацииСтроки` | +| `BeforeAddRow` | `ПередНачаломДобавления` | +| `BeforeDeleteRow` | `ПередУдалением` | +| `BeforeRowChange` | `ПередНачаломИзменения` | +| `OnStartEdit` | `ПриНачалеРедактирования` | +| `OnEndEdit` | `ПриОкончанииРедактирования` | +| `Selection` | `ВыборСтроки` | +| `OnCurrentPageChange` | `ПриСменеСтраницы` | +| `TextEditEnd` | `ОкончаниеВводаТекста` | + +Пример: элемент `Контрагент` + событие `OnChange` → обработчик `КонтрагентПриИзменении`. + +### 4.3. Типы элементов + +#### group — UsualGroup + +```json +{ "group": "horizontal", "name": "ГруппаШапка", "children": [ ... ] } +``` + +| Свойство | Тип | Описание | +|----------|-----|----------| +| `group` | string | Ориентация: `horizontal`, `vertical`, `alwaysHorizontal`, `alwaysVertical`, `collapsible` | +| `children` | array | Вложенные элементы | +| `showTitle` | bool | Показывать заголовок группы | +| `representation` | string | `none`, `normal`, `weak`, `strong` | +| `united` | bool | Объединение | + +#### input — InputField + +```json +{ "input": "Организация", "path": "Объект.Организация", "on": ["OnChange"] } +``` + +| Свойство | Тип | Описание | +|----------|-----|----------| +| `path` | string | DataPath | +| `multiLine` | bool | Многострочный режим | +| `passwordMode` | bool | Режим пароля | +| `titleLocation` | string | `none`, `left`, `right`, `top`, `bottom` | +| `choiceButton` | bool | Показывать кнопку выбора | +| `clearButton` | bool | Показывать кнопку очистки | +| `spinButton` | bool | Показывать кнопку прокрутки | +| `dropListButton` | bool | Показывать кнопку раскрытия | +| `markIncomplete` | bool | Автопометка незаполненных | +| `skipOnInput` | bool | Пропускать при вводе | +| `inputHint` | string | Подсказка ввода (placeholder) | +| `width` | int | Ширина | +| `height` | int | Высота | +| `horizontalStretch` | bool | Растягивание по горизонтали | +| `verticalStretch` | bool | Растягивание по вертикали | +| `autoMaxWidth` | bool | Автомаксимальная ширина | +| `autoMaxHeight` | bool | Автомаксимальная высота | + +#### check — CheckBoxField + +```json +{ "check": "ФлагАктивности", "path": "Активен", "on": ["OnChange"] } +``` + +| Свойство | Тип | Описание | +|----------|-----|----------| +| `path` | string | DataPath | +| `titleLocation` | string | Расположение заголовка | + +#### label — LabelDecoration + +```json +{ "label": "Подсказка", "title": "Выберите параметры", "hyperlink": true } +``` + +| Свойство | Тип | Описание | +|----------|-----|----------| +| `title` | string | Текст надписи | +| `hyperlink` | bool | Режим гиперссылки | +| `width` | int | Ширина | +| `height` | int | Высота | +| `autoMaxWidth` | bool | Автомаксимальная ширина | +| `autoMaxHeight` | bool | Автомаксимальная высота | + +#### labelField — LabelField + +```json +{ "labelField": "СтатусОбработки", "path": "Статус" } +``` + +| Свойство | Тип | Описание | +|----------|-----|----------| +| `path` | string | DataPath | +| `hyperlink` | bool | Режим гиперссылки | + +#### table — Table + +```json +{ + "table": "Товары", "path": "Объект.Товары", + "columns": [ + { "input": "Номенклатура", "path": "Объект.Товары.Номенклатура" } + ] +} +``` + +| Свойство | Тип | Описание | +|----------|-----|----------| +| `path` | string | DataPath | +| `columns` | array | Колонки (элементы input/check/labelField/picField) | +| `representation` | string | `List`, `Tree`, `HierarchicalList` | +| `changeRowSet` | bool | Разрешить добавление/удаление строк | +| `changeRowOrder` | bool | Разрешить перемещение строк | +| `height` | int | Высота в строках таблицы | +| `header` | bool | Показывать шапку | +| `footer` | bool | Показывать подвал | +| `commandBarLocation` | string | `None`, `Top`, `Bottom`, `Auto` | +| `searchStringLocation` | string | `None`, `Top`, `Bottom`, `CommandBar`, `Auto` | + +#### pages / page — Pages / Page + +```json +{ + "pages": "Страницы", "children": [ + { "page": "Основное", "children": [ ... ] }, + { "page": "Дополнительно", "children": [ ... ] } + ] +} +``` + +Page поддерживает `group` для задания ориентации содержимого и `children` для вложенных элементов. + +Pages поддерживает `pagesRepresentation`: `None`, `TabsOnTop`, `TabsOnBottom`, `TabsOnLeft`, `TabsOnRight`. + +#### button — Button + +```json +{ "button": "Загрузить", "command": "Загрузить", "defaultButton": true } +``` + +| Свойство | Тип | Описание | +|----------|-----|----------| +| `command` | string | Имя команды формы (→ `Form.Command.`) | +| `stdCommand` | string | Стандартная команда (→ `Form.StandardCommand.`) | +| `type` | string | `usual`, `hyperlink`, `commandBar` | +| `defaultButton` | bool | Кнопка по умолчанию | +| `picture` | string | Ссылка на картинку (`StdPicture.Name`) | +| `representation` | string | `Auto`, `Picture`, `Text`, `PictureAndText` | +| `locationInCommandBar` | string | `InCommandBar`, `InAdditionalSubmenu` | + +#### picture — PictureDecoration + +```json +{ "picture": "Логотип", "src": "CommonPicture.Логотип" } +``` + +| Свойство | Тип | Описание | +|----------|-----|----------| +| `src` или `picture` (как свойство) | string | Ссылка на картинку | +| `hyperlink` | bool | Режим гиперссылки | +| `width` | int | Ширина | +| `height` | int | Высота | + +#### picField — PictureField + +```json +{ "picField": "Фото", "path": "Фотография" } +``` + +#### calendar — CalendarField + +```json +{ "calendar": "Дата", "path": "ДатаОтчета" } +``` + +#### cmdBar — CommandBar + +```json +{ "cmdBar": "КоманднаяПанель", "children": [ ... ] } +``` + +#### popup — Popup + +```json +{ "popup": "Печать", "picture": "StdPicture.Print", "children": [ ... ] } +``` + +--- + +## 5. Attributes — реквизиты формы + +```json +"attributes": [ + { "name": "Объект", "type": "DocumentObject.Реализация", "main": true }, + { "name": "Итого", "type": "decimal(15,2)" }, + { "name": "Таблица", "type": "ValueTable", "columns": [ + { "name": "Номенклатура", "type": "CatalogRef.Номенклатура" }, + { "name": "Количество", "type": "decimal(10,3)" } + ]} +] +``` + +| Свойство | Тип | Описание | +|----------|-----|----------| +| `name` | string | Имя реквизита (обязательно) | +| `type` | string | Тип (shorthand) | +| `main` | bool | Основной реквизит формы | +| `title` | string | Заголовок | +| `savedData` | bool | Сохраняемые данные | +| `fillChecking` | string | `Show`, `DontShow` | +| `columns` | array | Колонки для ValueTable/ValueTree | + +--- + +## 6. Parameters — параметры формы + +```json +"parameters": [ + { "name": "Ключ", "type": "DocumentRef.Реализация", "key": true }, + { "name": "Основание", "type": "DocumentRef.Реализация" } +] +``` + +| Свойство | Тип | Описание | +|----------|-----|----------| +| `name` | string | Имя параметра (обязательно) | +| `type` | string | Тип (shorthand) | +| `key` | bool | Ключевой параметр | + +--- + +## 7. Commands — команды формы + +```json +"commands": [ + { "name": "Печать", "action": "ПечатьОбработка", "shortcut": "Ctrl+P" }, + { "name": "Обновить", "action": "ОбновитьОбработка", "picture": "StdPicture.Refresh" } +] +``` + +| Свойство | Тип | Описание | +|----------|-----|----------| +| `name` | string | Имя команды (обязательно) | +| `action` | string | Имя процедуры-обработчика | +| `title` | string | Заголовок | +| `shortcut` | string | Клавиатурное сочетание | +| `picture` | string | Ссылка на картинку | +| `representation` | string | `Auto`, `Picture`, `Text`, `PictureAndText` | + +--- + +## 8. Система типов (shorthand) + +### Примитивные типы + +| DSL | XML | +|-----|-----| +| `"string"` | `xs:string` (неограниченная) | +| `"string(100)"` | `xs:string` + Length=100 | +| `"decimal(15,2)"` | `xs:decimal` + Digits=15, FractionDigits=2, AllowedSign=Any | +| `"decimal(10,0,nonneg)"` | `xs:decimal` + AllowedSign=Nonnegative | +| `"boolean"` | `xs:boolean` | +| `"date"` | `xs:dateTime` + DateFractions=Date | +| `"dateTime"` | `xs:dateTime` + DateFractions=DateTime | +| `"time"` | `xs:dateTime` + DateFractions=Time | + +### Ссылочные типы + +| DSL | XML | +|-----|-----| +| `"CatalogRef.Организации"` | `cfg:CatalogRef.Организации` | +| `"DocumentObject.Реализация"` | `cfg:DocumentObject.Реализация` | +| `"EnumRef.СтавкиНДС"` | `cfg:EnumRef.СтавкиНДС` | +| `"DataProcessorObject.ЗагрузкаДанных"` | `cfg:DataProcessorObject.ЗагрузкаДанных` | + +### Платформенные типы + +| DSL | XML | +|-----|-----| +| `"ValueTable"` | `v8:ValueTable` | +| `"ValueTree"` | `v8:ValueTree` | +| `"ValueList"` | `v8:ValueListType` | +| `"FormattedString"` | `v8ui:FormattedString` | +| `"Picture"` | `v8ui:Picture` | +| `"DynamicList"` | `cfg:DynamicList` | + +### Составные типы + +Разделитель `" | "`: + +```json +"type": "CatalogRef.Организации | CatalogRef.ИндивидуальныеПредприниматели" +``` + +--- + +## 9. Автогенерация + +### Companion-элементы + +Для каждого элемента автоматически создаются служебные вложенные элементы: + +| Тип элемента | Companions | +|---|---| +| UsualGroup | ExtendedTooltip | +| InputField | ContextMenu, ExtendedTooltip | +| CheckBoxField | ContextMenu, ExtendedTooltip | +| LabelDecoration | ContextMenu, ExtendedTooltip | +| LabelField | ContextMenu, ExtendedTooltip | +| PictureDecoration | ContextMenu, ExtendedTooltip | +| PictureField | ContextMenu, ExtendedTooltip | +| CalendarField | ContextMenu, ExtendedTooltip | +| Table | ContextMenu, AutoCommandBar, SearchStringAddition, ViewStatusAddition, SearchControlAddition | +| Pages | ExtendedTooltip | +| Page | ExtendedTooltip | +| Button | ExtendedTooltip | + +Именование: `КонтекстноеМеню`, `РасширеннаяПодсказка`, `КоманднаяПанель`, `СтрокаПоиска`, `СостояниеПросмотра`, `УправлениеПоиском`. + +### ID + +Последовательная нумерация начиная с 1. `AutoCommandBar` формы всегда имеет `id="-1"`. + +### Namespace + +Все 17 namespace-деклараций добавляются автоматически (version="2.17"). + +### Кодировка + +UTF-8 с BOM (как в файлах конфигурации 1С). diff --git a/docs/form-guide.md b/docs/form-guide.md index cbc4e7ee..6eff4648 100644 --- a/docs/form-guide.md +++ b/docs/form-guide.md @@ -1,12 +1,13 @@ # Управляемые формы (Form) -Навыки группы `/form-*` позволяют анализировать управляемые формы 1С:Предприятия 8.3 из XML-исходников конфигурации, не читая тысячи строк XML. +Навыки группы `/form-*` позволяют анализировать и генерировать управляемые формы 1С:Предприятия 8.3 из XML-исходников, не читая тысячи строк XML. ## Навыки | Навык | Параметры | Описание | |-------|-----------|----------| | `/form-info` | `` | Компактная сводка: дерево элементов, реквизиты, команды, события | +| `/form-compile` | ` ` | Генерация Form.xml из компактного JSON-определения | ## Сценарии использования @@ -120,22 +121,68 @@ Claude сделает grep по Form.xml и найдёт полный XML-бло - `Таблица: ValueTable [Кол1: тип, Кол2: тип]` — таблица значений с колонками - `Список: DynamicList -> Catalog.Пользователи` — динамический список с основной таблицей +## Генерация формы с нуля + +`/form-compile` принимает компактное JSON-определение (20–50 строк) и генерирует полный Form.xml (100–500+ строк) с namespace-декларациями, companion-элементами и последовательными ID. + +``` +> Создай форму загрузки данных с полями Организация, Контрагент, таблицей Товары и кнопкой Загрузить +``` + +Claude создаст JSON-определение и вызовет `/form-compile`. Пример JSON: + +```json +{ + "properties": { "autoTitle": false }, + "events": { "OnCreateAtServer": "ПриСозданииНаСервере" }, + "elements": [ + { "group": "horizontal", "name": "Шапка", "children": [ + { "input": "Организация", "path": "Объект.Организация", "on": ["OnChange"] }, + { "input": "Контрагент", "path": "Объект.Контрагент" } + ]}, + { "table": "Товары", "path": "Объект.Товары", "columns": [ + { "input": "Номенклатура", "path": "Объект.Товары.Номенклатура" }, + { "input": "Сумма", "path": "Объект.Товары.Сумма", "readOnly": true } + ]}, + { "button": "Загрузить", "command": "Загрузить" } + ], + "attributes": [ + { "name": "Объект", "type": "DataProcessorObject.ЗагрузкаДанных", "main": true } + ], + "commands": [ + { "name": "Загрузить", "action": "ЗагрузитьОбработка" } + ] +} +``` + +### Верификация результата + +После компиляции используйте `/form-info` для проверки: + +``` +> /form-info src/МояОбработка/Forms/Форма/Ext/Form.xml +``` + +Структура в сводке должна совпадать с определением в JSON. + ## Примеры слеш-команд ``` > /form-info upload/acc_8.3.24/Documents/РеализацияТоваровУслуг/Forms/ФормаДокумента/Ext/Form.xml > /form-info src/МояОбработка/Forms/Форма/Ext/Form.xml +> /form-compile src/form.json src/МояОбработка/Forms/Форма/Ext/Form.xml ``` ## Связь с EPF-навыками -`/form-info` работает с формами из любых источников — конфигурации и внешних обработок. При работе с обработками: +`/form-info` и `/form-compile` работают с формами из любых источников — конфигурации и внешних обработок. При работе с обработками: -1. `/epf-add-form` — создать форму -2. `/form-info` — проанализировать существующую форму -3. Модифицировать Form.xml, ориентируясь на сводку +1. `/epf-add-form` — создать форму (каркас) +2. `/form-compile` — сгенерировать Form.xml из JSON-определения +3. `/form-info` — проверить результат 4. `/epf-build` — собрать EPF ## Спецификации - [Управляемая форма](1c-form-spec.md) — Form.xml, элементы, команды, реквизиты, система типов +- [Form DSL](form-dsl-spec.md) — JSON-формат описания формы для `/form-compile` From a813c7fd462e2b89ac90566f5a87ef175e67c777 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Mon, 9 Feb 2026 12:13:51 +0300 Subject: [PATCH 03/13] Add form-validate skill for structural validation of Form.xml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Checks: unique IDs (per pool), companion elements, DataPath→attribute refs, button→command refs, event handlers, command actions, MainAttribute. Co-Authored-By: Claude Opus 4.6 --- .claude/skills/form-validate/SKILL.md | 78 +++ .../form-validate/scripts/form-validate.ps1 | 486 ++++++++++++++++++ README.md | 3 +- docs/form-guide.md | 16 +- 4 files changed, 576 insertions(+), 7 deletions(-) create mode 100644 .claude/skills/form-validate/SKILL.md create mode 100644 .claude/skills/form-validate/scripts/form-validate.ps1 diff --git a/.claude/skills/form-validate/SKILL.md b/.claude/skills/form-validate/SKILL.md new file mode 100644 index 00000000..7c66e0bb --- /dev/null +++ b/.claude/skills/form-validate/SKILL.md @@ -0,0 +1,78 @@ +--- +name: form-validate +description: Валидация структурной корректности управляемой формы 1С (Form.xml) +argument-hint: +allowed-tools: + - Bash + - Read + - Glob +--- + +# /form-validate — Валидатор формы + +Проверяет Form.xml управляемой формы на структурные ошибки: уникальность ID, наличие companion-элементов, корректность ссылок DataPath и команд. + +## Использование + +``` +/form-validate +``` + +## Параметры + +| Параметр | Обязательный | По умолчанию | Описание | +|-----------|:------------:|--------------|-----------------------------| +| FormPath | да | — | Путь к файлу Form.xml | +| MaxErrors | нет | 30 | Остановиться после N ошибок | + +## Команда + +```powershell +powershell.exe -NoProfile -File .claude\skills\form-validate\scripts\form-validate.ps1 -FormPath "<путь>" +``` + +## Выполняемые проверки + +| # | Проверка | Серьёзность | +|---|---|---| +| 1 | Корневой элемент `
`, version="2.17" | ERROR / WARN | +| 2 | `` присутствует, id="-1" | ERROR | +| 3 | Уникальность ID элементов (отдельный пул) | ERROR | +| 4 | Уникальность ID реквизитов (отдельный пул) | ERROR | +| 5 | Уникальность ID команд (отдельный пул) | ERROR | +| 6 | Companion-элементы (ContextMenu, ExtendedTooltip, и др.) | ERROR | +| 7 | DataPath → ссылается на существующий реквизит | ERROR | +| 8 | CommandName кнопок → ссылается на существующую команду | ERROR | +| 9 | События имеют непустые имена обработчиков | ERROR | +| 10 | Команды имеют Action (обработчик) | ERROR | +| 11 | Не более одного MainAttribute | ERROR | + +## Вывод + +``` +=== Validation: ФормаДокумента === + +[OK] Root element: Form version=2.17 +[OK] AutoCommandBar: name='ФормаКоманднаяПанель', id=-1 +[OK] Unique element IDs: 96 elements +[OK] Unique attribute IDs: 38 entries +[OK] Unique command IDs: 5 entries +[OK] Companion elements: 86 elements checked +[OK] DataPath references: 53 paths checked +[OK] Command references: 2 buttons checked +[OK] Event handlers: 41 events checked +[OK] Command actions: 5 commands checked +[OK] MainAttribute: 1 main attribute + +--- +Total: 96 elements, 38 attributes, 5 commands +All checks passed. +``` + +Код возврата: 0 = все проверки пройдены, 1 = есть ошибки. + +## Когда использовать + +- **После `/form-compile`**: проверить корректность сгенерированной формы +- **После ручного редактирования Form.xml**: убедиться что ID уникальны, companions на месте, ссылки валидны +- **При отладке**: выявить ошибки в структуре формы до сборки EPF diff --git a/.claude/skills/form-validate/scripts/form-validate.ps1 b/.claude/skills/form-validate/scripts/form-validate.ps1 new file mode 100644 index 00000000..ca13dcdd --- /dev/null +++ b/.claude/skills/form-validate/scripts/form-validate.ps1 @@ -0,0 +1,486 @@ +param( + [Parameter(Mandatory)] + [string]$FormPath, + + [int]$MaxErrors = 30 +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- Load XML --- + +if (-not (Test-Path $FormPath)) { + Write-Error "File not found: $FormPath" + exit 1 +} + +$xmlDoc = New-Object System.Xml.XmlDocument +$xmlDoc.PreserveWhitespace = $false +try { + $xmlDoc.Load((Resolve-Path $FormPath).Path) +} catch { + Write-Host "[ERROR] XML parse error: $($_.Exception.Message)" + Write-Host "" + Write-Host "---" + Write-Host "Errors: 1, Warnings: 0" + exit 1 +} + +$nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) +$nsMgr.AddNamespace("f", "http://v8.1c.ru/8.3/xcf/logform") +$nsMgr.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core") + +$root = $xmlDoc.DocumentElement + +# --- Counters --- + +$errors = 0 +$warnings = 0 +$stopped = $false + +function Report-OK { + param([string]$msg) + Write-Host "[OK] $msg" +} + +function Report-Error { + param([string]$msg) + $script:errors++ + Write-Host "[ERROR] $msg" + if ($script:errors -ge $MaxErrors) { + $script:stopped = $true + } +} + +function Report-Warn { + param([string]$msg) + $script:warnings++ + Write-Host "[WARN] $msg" +} + +# --- Form name from path --- + +$formName = [System.IO.Path]::GetFileNameWithoutExtension($FormPath) +$parentDir = [System.IO.Path]::GetDirectoryName($FormPath) +if ($parentDir) { + $extDir = [System.IO.Path]::GetFileName($parentDir) + if ($extDir -eq "Ext") { + $formDir = [System.IO.Path]::GetDirectoryName($parentDir) + if ($formDir) { $formName = [System.IO.Path]::GetFileName($formDir) } + } +} + +Write-Host "=== Validation: $formName ===" +Write-Host "" + +# --- Check 1: Root element and version --- + +if ($root.LocalName -ne "Form") { + Report-Error "Root element is '$($root.LocalName)', expected 'Form'" +} else { + $version = $root.GetAttribute("version") + if ($version -eq "2.17") { + Report-OK "Root element: Form version=$version" + } elseif ($version) { + Report-Warn "Form version='$version' (expected 2.17)" + } else { + Report-Warn "Form version attribute missing" + } +} + +# --- Check 2: AutoCommandBar --- + +if (-not $stopped) { + $acb = $root.SelectSingleNode("f:AutoCommandBar", $nsMgr) + if ($acb) { + $acbName = $acb.GetAttribute("name") + $acbId = $acb.GetAttribute("id") + if ($acbId -eq "-1") { + Report-OK "AutoCommandBar: name='$acbName', id=$acbId" + } else { + Report-Error "AutoCommandBar id='$acbId', expected '-1'" + } + } else { + Report-Error "AutoCommandBar element missing" + } +} + +# --- Collect all elements with IDs --- + +$elementIds = @{} # id -> name (element ID pool) +$allElements = @() # @{Name; Tag; Id; ParentName; Node} + +function Collect-Elements { + param($node, [string]$parentName) + + foreach ($child in $node.ChildNodes) { + if ($child.NodeType -ne 'Element') { continue } + + $name = $child.GetAttribute("name") + $id = $child.GetAttribute("id") + + if ($name -and $id) { + $tag = $child.LocalName + + $script:allElements += @{ + Name = $name + Tag = $tag + Id = $id + ParentName = $parentName + Node = $child + } + + # Track element IDs (skip AutoCommandBar which has -1) + if ($id -ne "-1") { + if ($elementIds.ContainsKey($id)) { + Report-Error "Duplicate element id=${id}: '$name' and '$($elementIds[$id])'" + } else { + $elementIds[$id] = $name + } + } + + # Recurse into ChildItems + $childItems = $child.SelectSingleNode("f:ChildItems", $nsMgr) + if ($childItems) { + Collect-Elements -node $childItems -parentName $name + } + } + } +} + +# Collect from ChildItems +$childItemsRoot = $root.SelectSingleNode("f:ChildItems", $nsMgr) +if ($childItemsRoot) { + Collect-Elements -node $childItemsRoot -parentName "(root)" +} + +# Also collect from AutoCommandBar's ChildItems +$acb = $root.SelectSingleNode("f:AutoCommandBar", $nsMgr) +if ($acb) { + $acbChildren = $acb.SelectSingleNode("f:ChildItems", $nsMgr) + if ($acbChildren) { + Collect-Elements -node $acbChildren -parentName "ФормаКоманднаяПанель" + } +} + +# --- Check 3: Unique element IDs --- + +if (-not $stopped) { + $dupCount = ($allElements | Group-Object { $_.Id } | Where-Object { $_.Count -gt 1 -and $_.Name -ne "-1" }).Count + if ($dupCount -eq 0) { + Report-OK "Unique element IDs: $($elementIds.Count) elements" + } +} + +# --- Collect attributes (separate ID pool) --- + +$attrMap = @{} # name -> node +$attrIds = @{} # id -> name +$attrNodes = $root.SelectNodes("f:Attributes/f:Attribute", $nsMgr) +foreach ($attr in $attrNodes) { + $attrName = $attr.GetAttribute("name") + $attrId = $attr.GetAttribute("id") + if ($attrName) { + $attrMap[$attrName] = $attr + } + if ($attrId -and $attrId -ne "") { + if ($attrIds.ContainsKey($attrId)) { + Report-Error "Duplicate attribute id=${attrId}: '$attrName' and '$($attrIds[$attrId])'" + } else { + $attrIds[$attrId] = $attrName + } + } + + # Column IDs are a separate sub-pool per attribute — check uniqueness within parent + $colIds = @{} + foreach ($col in $attr.SelectNodes("f:Columns/f:Column", $nsMgr)) { + $colId = $col.GetAttribute("id") + $colName = $col.GetAttribute("name") + if ($colId -and $colId -ne "") { + if ($colIds.ContainsKey($colId)) { + Report-Error "Duplicate column id=${colId} in '$attrName': '$colName' and '$($colIds[$colId])'" + } else { + $colIds[$colId] = $colName + } + } + } +} + +if (-not $stopped) { + $attrDupCount = ($attrIds.GetEnumerator() | Group-Object Value | Where-Object { $_.Count -gt 1 }).Count + if ($attrDupCount -eq 0 -and $attrIds.Count -gt 0) { + Report-OK "Unique attribute IDs: $($attrIds.Count) entries" + } +} + +# --- Collect commands (separate ID pool) --- + +$cmdMap = @{} # name -> node +$cmdIds = @{} # id -> name +$cmdNodes = $root.SelectNodes("f:Commands/f:Command", $nsMgr) +foreach ($cmd in $cmdNodes) { + $cmdName = $cmd.GetAttribute("name") + $cmdId = $cmd.GetAttribute("id") + if ($cmdName) { + $cmdMap[$cmdName] = $cmd + } + if ($cmdId -and $cmdId -ne "") { + if ($cmdIds.ContainsKey($cmdId)) { + Report-Error "Duplicate command id=${cmdId}: '$cmdName' and '$($cmdIds[$cmdId])'" + } else { + $cmdIds[$cmdId] = $cmdName + } + } +} + +if (-not $stopped) { + if ($cmdIds.Count -gt 0) { + $cmdDupCount = ($cmdIds.GetEnumerator() | Group-Object Value | Where-Object { $_.Count -gt 1 }).Count + if ($cmdDupCount -eq 0) { + Report-OK "Unique command IDs: $($cmdIds.Count) entries" + } + } +} + +# --- Check 4: Companion elements --- + +# Define required companions per element type +$companionRules = @{ + "InputField" = @("ContextMenu", "ExtendedTooltip") + "CheckBoxField" = @("ContextMenu", "ExtendedTooltip") + "LabelDecoration" = @("ContextMenu", "ExtendedTooltip") + "LabelField" = @("ContextMenu", "ExtendedTooltip") + "PictureDecoration" = @("ContextMenu", "ExtendedTooltip") + "PictureField" = @("ContextMenu", "ExtendedTooltip") + "CalendarField" = @("ContextMenu", "ExtendedTooltip") + "UsualGroup" = @("ExtendedTooltip") + "Pages" = @("ExtendedTooltip") + "Page" = @("ExtendedTooltip") + "Button" = @("ExtendedTooltip") + "Table" = @("ContextMenu", "AutoCommandBar", "SearchStringAddition", "ViewStatusAddition", "SearchControlAddition") +} + +if (-not $stopped) { + $companionErrors = 0 + $companionChecked = 0 + + foreach ($el in $allElements) { + if ($stopped) { break } + $tag = $el.Tag + $elName = $el.Name + $node = $el.Node + + if (-not $companionRules.ContainsKey($tag)) { continue } + + $required = $companionRules[$tag] + $companionChecked++ + + foreach ($compTag in $required) { + $compNode = $node.SelectSingleNode("f:$compTag", $nsMgr) + if (-not $compNode) { + Report-Error "[$tag] '$elName': missing companion <$compTag>" + $companionErrors++ + } + } + } + + if ($companionErrors -eq 0 -and $companionChecked -gt 0) { + Report-OK "Companion elements: $companionChecked elements checked" + } +} + +# --- Check 5: DataPath -> Attribute references --- + +if (-not $stopped) { + $pathErrors = 0 + $pathChecked = 0 + + foreach ($el in $allElements) { + if ($stopped) { break } + $tag = $el.Tag + $elName = $el.Name + $node = $el.Node + + # Skip companion elements + if ($tag -in @("ContextMenu", "ExtendedTooltip", "AutoCommandBar", "SearchStringAddition", "ViewStatusAddition", "SearchControlAddition")) { + continue + } + + $dpNode = $node.SelectSingleNode("f:DataPath", $nsMgr) + if (-not $dpNode) { continue } + + $dataPath = $dpNode.InnerText.Trim() + if (-not $dataPath) { continue } + + $pathChecked++ + + # Extract root segment of path, strip array indices like [0] + $cleanPath = $dataPath -replace '\[\d+\]', '' + $segments = $cleanPath -split '\.' + $rootAttr = $segments[0] + + if (-not $attrMap.ContainsKey($rootAttr)) { + Report-Error "[$tag] '$elName': DataPath='$dataPath' — attribute '$rootAttr' not found" + $pathErrors++ + } + } + + if ($pathErrors -eq 0 -and $pathChecked -gt 0) { + Report-OK "DataPath references: $pathChecked paths checked" + } elseif ($pathChecked -eq 0) { + Report-OK "DataPath references: none" + } +} + +# --- Check 6: Button command references --- + +if (-not $stopped) { + $cmdErrors = 0 + $cmdChecked = 0 + + foreach ($el in $allElements) { + if ($stopped) { break } + $tag = $el.Tag + $elName = $el.Name + $node = $el.Node + + if ($tag -ne "Button") { continue } + + $cmdNode = $node.SelectSingleNode("f:CommandName", $nsMgr) + if (-not $cmdNode) { continue } + + $cmdRef = $cmdNode.InnerText.Trim() + if (-not $cmdRef) { continue } + + # Form.Command.XXX -> check command XXX exists + if ($cmdRef -match '^Form\.Command\.(.+)$') { + $cmdName = $Matches[1] + $cmdChecked++ + if (-not $cmdMap.ContainsKey($cmdName)) { + Report-Error "[Button] '$elName': CommandName='$cmdRef' — command '$cmdName' not found in Commands" + $cmdErrors++ + } + } + # Form.StandardCommand.XXX — skip, standard commands always exist + } + + if ($cmdErrors -eq 0 -and $cmdChecked -gt 0) { + Report-OK "Command references: $cmdChecked buttons checked" + } elseif ($cmdChecked -eq 0) { + Report-OK "Command references: none" + } +} + +# --- Check 7: Events have handler names --- + +if (-not $stopped) { + $eventErrors = 0 + $eventChecked = 0 + + # Form-level events + $formEvents = $root.SelectSingleNode("f:Events", $nsMgr) + if ($formEvents) { + foreach ($evt in $formEvents.SelectNodes("f:Event", $nsMgr)) { + $evtName = $evt.GetAttribute("name") + $handler = $evt.InnerText.Trim() + $eventChecked++ + if (-not $handler) { + Report-Error "Form event '$evtName': empty handler name" + $eventErrors++ + } + } + } + + # Element-level events + foreach ($el in $allElements) { + if ($stopped) { break } + $tag = $el.Tag + $elName = $el.Name + $node = $el.Node + + $eventsNode = $node.SelectSingleNode("f:Events", $nsMgr) + if (-not $eventsNode) { continue } + + foreach ($evt in $eventsNode.SelectNodes("f:Event", $nsMgr)) { + $evtName = $evt.GetAttribute("name") + $handler = $evt.InnerText.Trim() + $eventChecked++ + if (-not $handler) { + Report-Error "[$tag] '$elName' event '$evtName': empty handler name" + $eventErrors++ + } + } + } + + if ($eventErrors -eq 0 -and $eventChecked -gt 0) { + Report-OK "Event handlers: $eventChecked events checked" + } elseif ($eventChecked -eq 0) { + Report-OK "Event handlers: none" + } +} + +# --- Check 8: Command actions --- + +if (-not $stopped) { + $actionErrors = 0 + $actionChecked = 0 + + foreach ($cmd in $cmdNodes) { + if ($stopped) { break } + $cmdName = $cmd.GetAttribute("name") + $actionNode = $cmd.SelectSingleNode("f:Action", $nsMgr) + $actionChecked++ + if (-not $actionNode -or -not $actionNode.InnerText.Trim()) { + Report-Error "Command '$cmdName': missing or empty Action" + $actionErrors++ + } + } + + if ($actionErrors -eq 0 -and $actionChecked -gt 0) { + Report-OK "Command actions: $actionChecked commands checked" + } elseif ($actionChecked -eq 0) { + Report-OK "Command actions: none" + } +} + +# --- Check 9: MainAttribute count --- + +if (-not $stopped) { + $mainCount = 0 + foreach ($attr in $attrNodes) { + $mainNode = $attr.SelectSingleNode("f:MainAttribute", $nsMgr) + if ($mainNode -and $mainNode.InnerText -eq "true") { + $mainCount++ + } + } + + if ($mainCount -le 1) { + $mainInfo = if ($mainCount -eq 1) { "1 main attribute" } else { "no main attribute" } + Report-OK "MainAttribute: $mainInfo" + } else { + Report-Error "Multiple MainAttribute=true ($mainCount found, expected 0 or 1)" + } +} + +# --- Summary --- + +Write-Host "" +Write-Host "---" +Write-Host "Total: $($allElements.Count) elements, $($attrNodes.Count) attributes, $($cmdNodes.Count) commands" + +if ($stopped) { + Write-Host "Stopped after $MaxErrors errors. Fix and re-run." +} + +if ($errors -eq 0 -and $warnings -eq 0) { + Write-Host "All checks passed." +} else { + Write-Host "Errors: $errors, Warnings: $warnings" +} + +if ($errors -gt 0) { + exit 1 +} else { + exit 0 +} diff --git a/README.md b/README.md index 087724ca..65c70c55 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ |--------|--------|----------|------| | Внешние обработки (EPF) | 10 навыков `/epf-*` | Создание, модификация, сборка обработок из XML-исходников | [Подробнее](docs/epf-guide.md) | | Табличный документ (MXL) | 4 навыка `/mxl-*` | Анализ, создание, компиляция макетов печатных форм | [Подробнее](docs/mxl-guide.md) | -| Управляемые формы (Form) | 2 навыка `/form-*` | Анализ и генерация управляемых форм из XML-исходников | [Подробнее](docs/form-guide.md) | +| Управляемые формы (Form) | 3 навыка `/form-*` | Анализ, генерация, валидация управляемых форм | [Подробнее](docs/form-guide.md) | | Утилиты | `/img-grid` | Наложение сетки на изображение для определения пропорций колонок | — | ## Требования @@ -59,6 +59,7 @@ ├── mxl-decompile/ # Декомпиляция макета в JSON ├── form-info/ # Анализ структуры управляемой формы ├── form-compile/ # Компиляция формы из JSON +├── form-validate/ # Валидация формы └── img-grid/ # Сетка для анализа изображений docs/ ├── epf-guide.md # Гайд: внешние обработки diff --git a/docs/form-guide.md b/docs/form-guide.md index 6eff4648..f0d8087c 100644 --- a/docs/form-guide.md +++ b/docs/form-guide.md @@ -1,6 +1,6 @@ # Управляемые формы (Form) -Навыки группы `/form-*` позволяют анализировать и генерировать управляемые формы 1С:Предприятия 8.3 из XML-исходников, не читая тысячи строк XML. +Навыки группы `/form-*` позволяют анализировать, генерировать и валидировать управляемые формы 1С:Предприятия 8.3 из XML-исходников, не читая тысячи строк XML. ## Навыки @@ -8,6 +8,7 @@ |-------|-----------|----------| | `/form-info` | `` | Компактная сводка: дерево элементов, реквизиты, команды, события | | `/form-compile` | ` ` | Генерация Form.xml из компактного JSON-определения | +| `/form-validate` | `` | Валидация: уникальность ID, companions, DataPath, команды | ## Сценарии использования @@ -157,13 +158,14 @@ Claude создаст JSON-определение и вызовет `/form-compi ### Верификация результата -После компиляции используйте `/form-info` для проверки: +После компиляции используйте `/form-validate` и `/form-info` для проверки: ``` +> /form-validate src/МояОбработка/Forms/Форма/Ext/Form.xml > /form-info src/МояОбработка/Forms/Форма/Ext/Form.xml ``` -Структура в сводке должна совпадать с определением в JSON. +`/form-validate` проверит структурную корректность (ID, companions, ссылки), `/form-info` покажет результат в виде компактной сводки. ## Примеры слеш-команд @@ -171,16 +173,18 @@ Claude создаст JSON-определение и вызовет `/form-compi > /form-info upload/acc_8.3.24/Documents/РеализацияТоваровУслуг/Forms/ФормаДокумента/Ext/Form.xml > /form-info src/МояОбработка/Forms/Форма/Ext/Form.xml > /form-compile src/form.json src/МояОбработка/Forms/Форма/Ext/Form.xml +> /form-validate src/МояОбработка/Forms/Форма/Ext/Form.xml ``` ## Связь с EPF-навыками -`/form-info` и `/form-compile` работают с формами из любых источников — конфигурации и внешних обработок. При работе с обработками: +Навыки `/form-*` работают с формами из любых источников — конфигурации и внешних обработок. При работе с обработками: 1. `/epf-add-form` — создать форму (каркас) 2. `/form-compile` — сгенерировать Form.xml из JSON-определения -3. `/form-info` — проверить результат -4. `/epf-build` — собрать EPF +3. `/form-validate` — проверить корректность +4. `/form-info` — проанализировать результат +5. `/epf-build` — собрать EPF ## Спецификации From 99f57a5ff29f1e5ef5b2baf7b79a1e47e0a7f367 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Mon, 9 Feb 2026 13:34:56 +0300 Subject: [PATCH 04/13] Add form-add skill for modifying existing Form.xml Adds elements, attributes, and commands to existing managed forms via JSON input. Supports positional insertion (into/after), auto ID allocation from correct pools, companion generation, and event handlers. Co-Authored-By: Claude Opus 4.6 --- .claude/skills/form-add/SKILL.md | 110 +++ .claude/skills/form-add/scripts/form-add.ps1 | 958 +++++++++++++++++++ README.md | 3 +- docs/form-guide.md | 36 +- 4 files changed, 1098 insertions(+), 9 deletions(-) create mode 100644 .claude/skills/form-add/SKILL.md create mode 100644 .claude/skills/form-add/scripts/form-add.ps1 diff --git a/.claude/skills/form-add/SKILL.md b/.claude/skills/form-add/SKILL.md new file mode 100644 index 00000000..0de9be7f --- /dev/null +++ b/.claude/skills/form-add/SKILL.md @@ -0,0 +1,110 @@ +--- +name: form-add +description: Добавление элементов, реквизитов и команд в существующую управляемую форму 1С (Form.xml) +argument-hint: +allowed-tools: + - Bash + - Read + - Write + - Glob +--- + +# /form-add — Добавление в форму + +Добавляет элементы, реквизиты и/или команды в существующий Form.xml. Автоматически выделяет ID из правильного пула, генерирует companion-элементы (ContextMenu, ExtendedTooltip, и др.) и обработчики событий. + +## Использование + +``` +/form-add +``` + +## Параметры + +| Параметр | Обязательный | Описание | +|-----------|:------------:|----------------------------------| +| FormPath | да | Путь к существующему Form.xml | +| JsonPath | да | Путь к JSON с описанием добавлений | + +## Команда + +```powershell +powershell.exe -NoProfile -File .claude\skills\form-add\scripts\form-add.ps1 -FormPath "<путь>" -JsonPath "<путь>" +``` + +## JSON формат + +```json +{ + "into": "ГруппаШапка", + "after": "Контрагент", + "elements": [ + { "input": "Склад", "path": "Объект.Склад", "on": ["OnChange"] } + ], + "attributes": [ + { "name": "СуммаИтого", "type": "decimal(15,2)" } + ], + "commands": [ + { "name": "Рассчитать", "action": "РассчитатьОбработка" } + ] +} +``` + +### Позиционирование элементов + +| Ключ | По умолчанию | Описание | +|------|-------------|----------| +| `into` | корневой ChildItems | Имя группы/таблицы/страницы, куда вставлять | +| `after` | в конец | Имя элемента, после которого вставлять | + +### Типы элементов + +Те же DSL-ключи, что в `/form-compile`: + +| Ключ | XML тег | Companions | +|------|---------|------------| +| `input` | InputField | ContextMenu, ExtendedTooltip | +| `check` | CheckBoxField | ContextMenu, ExtendedTooltip | +| `label` | LabelDecoration | ContextMenu, ExtendedTooltip | +| `labelField` | LabelField | ContextMenu, ExtendedTooltip | +| `group` | UsualGroup | ExtendedTooltip | +| `table` | Table | ContextMenu, AutoCommandBar, Search*, ViewStatus* | +| `pages` | Pages | ExtendedTooltip | +| `page` | Page | ExtendedTooltip | +| `button` | Button | ExtendedTooltip | + +Группы и таблицы поддерживают `children`/`columns` для вложенных элементов. + +### Система типов (для attributes) + +`string`, `string(100)`, `decimal(15,2)`, `boolean`, `date`, `dateTime`, `CatalogRef.XXX`, `DocumentObject.XXX`, `ValueTable`, `DynamicList`, `Type1 | Type2` (составной). + +## Вывод + +``` +=== form-add: Форма === + +Added elements (into ГруппаШапка, after Контрагент): + + [Input] Склад -> Объект.Склад {OnChange} + +Added attributes: + + СуммаИтого: decimal(15,2) (id=12) + +--- +Total: 1 element(s) (+2 companions), 1 attribute(s) +Run /form-validate to verify. +``` + +## Когда использовать + +- **После `/form-compile`**: добавить элементы, которые не были в исходном JSON +- **Модификация существующих форм**: добавить поле, реквизит или команду в форму из конфигурации +- **Пакетное добавление**: один JSON может содержать элементы + реквизиты + команды + +## Workflow + +1. `/form-info` — посмотреть текущую структуру формы +2. Создать JSON с описанием добавлений +3. `/form-add` — добавить в форму +4. `/form-validate` — проверить корректность +5. `/form-info` — убедиться что добавилось правильно diff --git a/.claude/skills/form-add/scripts/form-add.ps1 b/.claude/skills/form-add/scripts/form-add.ps1 new file mode 100644 index 00000000..7f73b800 --- /dev/null +++ b/.claude/skills/form-add/scripts/form-add.ps1 @@ -0,0 +1,958 @@ +param( + [Parameter(Mandatory)] + [string]$FormPath, + + [Parameter(Mandatory)] + [string]$JsonPath +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# === 1. Load Form.xml === + +if (-not (Test-Path $FormPath)) { + Write-Error "File not found: $FormPath" + exit 1 +} +if (-not (Test-Path $JsonPath)) { + Write-Error "File not found: $JsonPath" + exit 1 +} + +$resolvedFormPath = (Resolve-Path $FormPath).Path +$xmlDoc = New-Object System.Xml.XmlDocument +$xmlDoc.PreserveWhitespace = $true +try { + $xmlDoc.Load($resolvedFormPath) +} catch { + Write-Host "[ERROR] XML parse error: $($_.Exception.Message)" + exit 1 +} + +$formNs = "http://v8.1c.ru/8.3/xcf/logform" +$v8Ns = "http://v8.1c.ru/8.1/data/core" +$nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) +$nsMgr.AddNamespace("f", $formNs) +$nsMgr.AddNamespace("v8", $v8Ns) + +$root = $xmlDoc.DocumentElement + +# === 2. Load JSON === + +$def = Get-Content -Raw -Encoding UTF8 $JsonPath | ConvertFrom-Json + +# === 3. Form name + header === + +$formName = [System.IO.Path]::GetFileNameWithoutExtension($FormPath) +$parentDir = [System.IO.Path]::GetDirectoryName($resolvedFormPath) +if ($parentDir) { + $extDir = [System.IO.Path]::GetFileName($parentDir) + if ($extDir -eq "Ext") { + $formDir = [System.IO.Path]::GetDirectoryName($parentDir) + if ($formDir) { $formName = [System.IO.Path]::GetFileName($formDir) } + } +} + +Write-Host "=== form-add: $formName ===" +Write-Host "" + +# === 4. Scan max IDs per pool === + +$script:nextElemId = 0 +$script:nextAttrId = 0 +$script:nextCmdId = 0 + +function Scan-ElementIds($node) { + foreach ($child in $node.ChildNodes) { + if ($child.NodeType -ne 'Element') { continue } + $id = $child.GetAttribute("id") + if ($id -and $id -ne "" -and $id -ne "-1") { + try { + $intId = [int]$id + if ($intId -gt $script:nextElemId) { $script:nextElemId = $intId } + } catch {} + } + $ci = $child.SelectSingleNode("f:ChildItems", $nsMgr) + if ($ci) { Scan-ElementIds $ci } + } +} + +$rootCI = $root.SelectSingleNode("f:ChildItems", $nsMgr) +if ($rootCI) { Scan-ElementIds $rootCI } + +# Also scan AutoCommandBar children +$acb = $root.SelectSingleNode("f:AutoCommandBar", $nsMgr) +if ($acb) { + $acbCI = $acb.SelectSingleNode("f:ChildItems", $nsMgr) + if ($acbCI) { Scan-ElementIds $acbCI } +} + +# Scan attribute IDs +foreach ($attr in $root.SelectNodes("f:Attributes/f:Attribute", $nsMgr)) { + $id = $attr.GetAttribute("id") + if ($id) { + try { $intId = [int]$id; if ($intId -gt $script:nextAttrId) { $script:nextAttrId = $intId } } catch {} + } +} + +# Scan command IDs +foreach ($cmd in $root.SelectNodes("f:Commands/f:Command", $nsMgr)) { + $id = $cmd.GetAttribute("id") + if ($id) { + try { $intId = [int]$id; if ($intId -gt $script:nextCmdId) { $script:nextCmdId = $intId } } catch {} + } +} + +$script:nextElemId++ +$script:nextAttrId++ +$script:nextCmdId++ + +function New-ElemId { $id = $script:nextElemId; $script:nextElemId++; return $id } +function New-AttrId { $id = $script:nextAttrId; $script:nextAttrId++; return $id } +function New-CmdId { $id = $script:nextCmdId; $script:nextCmdId++; return $id } + +# For element emitters, New-Id = New-ElemId +function New-Id { return New-ElemId } + +# === 5. Fragment helpers (StringBuilder + Emit-* from form-compile) === + +$script:xml = New-Object System.Text.StringBuilder 4096 + +function X { + param([string]$text) + $script:xml.AppendLine($text) | Out-Null +} + +function Esc-Xml { + param([string]$s) + return $s.Replace('&','&').Replace('<','<').Replace('>','>').Replace('"','"') +} + +function Emit-MLText { + param([string]$tag, [string]$text, [string]$indent) + X "$indent<$tag>" + X "$indent`t" + X "$indent`t`tru" + X "$indent`t`t$(Esc-Xml $text)" + X "$indent`t" + X "$indent" +} + +# --- Type emitter --- + +function Emit-Type { + param($typeStr, [string]$indent) + if (-not $typeStr) { X "$indent"; return } + $typeString = "$typeStr" + $parts = $typeString -split '\s*\|\s*' + X "$indent" + foreach ($part in $parts) { + Emit-SingleType -typeStr $part.Trim() -indent "$indent`t" + } + X "$indent" +} + +function Emit-SingleType { + param([string]$typeStr, [string]$indent) + + if ($typeStr -eq "boolean") { + X "$indentxs:boolean"; return + } + if ($typeStr -match '^string(\((\d+)\))?$') { + $len = if ($Matches[2]) { $Matches[2] } else { "0" } + X "$indentxs:string" + X "$indent" + X "$indent`t$len" + X "$indent`tVariable" + X "$indent"; return + } + if ($typeStr -match '^decimal\((\d+),(\d+)(,nonneg)?\)$') { + $digits = $Matches[1]; $fraction = $Matches[2] + $sign = if ($Matches[3]) { "Nonnegative" } else { "Any" } + X "$indentxs:decimal" + X "$indent" + X "$indent`t$digits" + X "$indent`t$fraction" + X "$indent`t$sign" + X "$indent"; return + } + if ($typeStr -match '^(date|dateTime|time)$') { + $fractions = switch ($typeStr) { "date" { "Date" } "dateTime" { "DateTime" } "time" { "Time" } } + X "$indentxs:dateTime" + X "$indent" + X "$indent`t$fractions" + X "$indent"; return + } + $v8Types = @{ + "ValueTable" = "v8:ValueTable"; "ValueTree" = "v8:ValueTree"; "ValueList" = "v8:ValueListType" + "TypeDescription" = "v8:TypeDescription"; "Universal" = "v8:Universal" + "FixedArray" = "v8:FixedArray"; "FixedStructure" = "v8:FixedStructure" + } + if ($v8Types.ContainsKey($typeStr)) { X "$indent$($v8Types[$typeStr])"; return } + $uiTypes = @{ "FormattedString" = "v8ui:FormattedString"; "Picture" = "v8ui:Picture"; "Color" = "v8ui:Color"; "Font" = "v8ui:Font" } + if ($uiTypes.ContainsKey($typeStr)) { X "$indent$($uiTypes[$typeStr])"; return } + if ($typeStr -eq "DynamicList") { X "$indentcfg:DynamicList"; return } + if ($typeStr -match '^DataComposition') { + $dcsMap = @{ "DataCompositionSettings" = "dcsset:DataCompositionSettings"; "DataCompositionSchema" = "dcssch:DataCompositionSchema"; "DataCompositionComparisonType" = "dcscor:DataCompositionComparisonType" } + if ($dcsMap.ContainsKey($typeStr)) { X "$indent$($dcsMap[$typeStr])"; return } + } + if ($typeStr -match '^(CatalogRef|CatalogObject|DocumentRef|DocumentObject|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|TaskRef|InformationRegisterRecordSet|AccumulationRegisterRecordSet|DataProcessorObject)\.') { + X "$indentcfg:$typeStr"; return + } + if ($typeStr.Contains('.')) { X "$indentcfg:$typeStr" } + else { X "$indent$typeStr" } +} + +# --- Event handler name generator --- + +$script:eventSuffixMap = @{ + "OnChange" = "ПриИзменении"; "StartChoice" = "НачалоВыбора"; "ChoiceProcessing" = "ОбработкаВыбора" + "AutoComplete" = "АвтоПодбор"; "Clearing" = "Очистка"; "Opening" = "Открытие"; "Click" = "Нажатие" + "OnActivateRow" = "ПриАктивизацииСтроки"; "BeforeAddRow" = "ПередНачаломДобавления" + "BeforeDeleteRow" = "ПередУдалением"; "BeforeRowChange" = "ПередНачаломИзменения" + "OnStartEdit" = "ПриНачалеРедактирования"; "OnEndEdit" = "ПриОкончанииРедактирования" + "Selection" = "ВыборСтроки"; "OnCurrentPageChange" = "ПриСменеСтраницы" + "TextEditEnd" = "ОкончаниеВводаТекста"; "URLProcessing" = "ОбработкаНавигационнойСсылки" + "DragStart" = "НачалоПеретаскивания"; "Drag" = "Перетаскивание" + "DragCheck" = "ПроверкаПеретаскивания"; "Drop" = "Помещение"; "AfterDeleteRow" = "ПослеУдаления" +} + +function Get-HandlerName { + param([string]$elementName, [string]$eventName) + $suffix = $script:eventSuffixMap[$eventName] + if ($suffix) { return "$elementName$suffix" } + return "$elementName$eventName" +} + +# --- Element helpers --- + +function Get-ElementName { + param($el, [string]$typeKey) + if ($el.name) { return "$($el.name)" } + return "$($el.$typeKey)" +} + +function Emit-Events { + param($el, [string]$elementName, [string]$indent) + if (-not $el.on) { return } + X "$indent" + foreach ($evt in $el.on) { + $evtName = "$evt" + $handler = if ($el.handlers -and $el.handlers.$evtName) { "$($el.handlers.$evtName)" } + else { Get-HandlerName -elementName $elementName -eventName $evtName } + X "$indent`t$handler" + } + X "$indent" +} + +function Emit-Companion { + param([string]$tag, [string]$name, [string]$indent) + $id = New-Id + X "$indent<$tag name=`"$name`" id=`"$id`"/>" +} + +function Emit-CommonFlags { + param($el, [string]$indent) + if ($el.hidden -eq $true) { X "$indentfalse" } + if ($el.disabled -eq $true) { X "$indentfalse" } + if ($el.readOnly -eq $true) { X "$indenttrue" } +} + +function Emit-Title { + param($el, [string]$name, [string]$indent) + if ($el.title) { Emit-MLText -tag "Title" -text "$($el.title)" -indent $indent } +} + +# --- Element emitters --- + +function Emit-Group { + param($el, [string]$name, [int]$id, [string]$indent) + X "$indent" + $inner = "$indent`t" + Emit-Title -el $el -name $name -indent $inner + $groupVal = "$($el.group)" + $orientation = switch ($groupVal) { + "horizontal" { "Horizontal" } "vertical" { "Vertical" } + "alwaysHorizontal" { "AlwaysHorizontal" } "alwaysVertical" { "AlwaysVertical" } + default { $null } + } + if ($orientation) { X "$inner$orientation" } + if ($groupVal -eq "collapsible") { X "$innerVertical"; X "$innerCollapsible" } + if ($el.representation) { + $repr = switch ("$($el.representation)") { "none" { "None" } "normal" { "NormalSeparation" } "weak" { "WeakSeparation" } "strong" { "StrongSeparation" } default { "$($el.representation)" } } + X "$inner$repr" + } + if ($el.showTitle -eq $false) { X "$innerfalse" } + if ($el.united -eq $false) { X "$innerfalse" } + Emit-CommonFlags -el $el -indent $inner + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner + if ($el.children -and $el.children.Count -gt 0) { + X "$inner" + foreach ($child in $el.children) { Emit-Element -el $child -indent "$inner`t" } + X "$inner" + } + X "$indent" +} + +function Emit-Input { + param($el, [string]$name, [int]$id, [string]$indent) + X "$indent" + $inner = "$indent`t" + if ($el.path) { X "$inner$($el.path)" } + Emit-Title -el $el -name $name -indent $inner + Emit-CommonFlags -el $el -indent $inner + if ($el.titleLocation) { + $loc = switch ("$($el.titleLocation)") { "none" { "None" } "left" { "Left" } "right" { "Right" } "top" { "Top" } "bottom" { "Bottom" } default { "$($el.titleLocation)" } } + X "$inner$loc" + } + if ($el.multiLine -eq $true) { X "$innertrue" } + if ($el.passwordMode -eq $true) { X "$innertrue" } + if ($el.choiceButton -eq $false) { X "$innerfalse" } + if ($el.clearButton -eq $true) { X "$innertrue" } + if ($el.spinButton -eq $true) { X "$innertrue" } + if ($el.dropListButton -eq $true) { X "$innertrue" } + if ($el.markIncomplete -eq $true) { X "$innertrue" } + if ($el.skipOnInput -eq $true) { X "$innertrue" } + if ($el.autoMaxWidth -eq $false) { X "$innerfalse" } + if ($el.autoMaxHeight -eq $false) { X "$innerfalse" } + if ($el.width) { X "$inner$($el.width)" } + if ($el.height) { X "$inner$($el.height)" } + if ($el.horizontalStretch -eq $true) { X "$innertrue" } + if ($el.verticalStretch -eq $true) { X "$innertrue" } + if ($el.inputHint) { Emit-MLText -tag "InputHint" -text "$($el.inputHint)" -indent $inner } + Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner + Emit-Events -el $el -elementName $name -indent $inner + X "$indent" +} + +function Emit-Check { + param($el, [string]$name, [int]$id, [string]$indent) + X "$indent" + $inner = "$indent`t" + if ($el.path) { X "$inner$($el.path)" } + Emit-Title -el $el -name $name -indent $inner + Emit-CommonFlags -el $el -indent $inner + if ($el.titleLocation) { X "$inner$($el.titleLocation)" } + Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner + Emit-Events -el $el -elementName $name -indent $inner + X "$indent" +} + +function Emit-Label { + param($el, [string]$name, [int]$id, [string]$indent) + X "$indent" + $inner = "$indent`t" + if ($el.title) { + $formatted = if ($el.hyperlink -eq $true) { "true" } else { "false" } + X "$inner" + X "$inner`t<v8:item>" + X "$inner`t`t<v8:lang>ru</v8:lang>" + X "$inner`t`t<v8:content>$(Esc-Xml "$($el.title)")</v8:content>" + X "$inner`t</v8:item>" + X "$inner" + } + Emit-CommonFlags -el $el -indent $inner + if ($el.hyperlink -eq $true) { X "$innertrue" } + if ($el.autoMaxWidth -eq $false) { X "$innerfalse" } + if ($el.autoMaxHeight -eq $false) { X "$innerfalse" } + if ($el.width) { X "$inner$($el.width)" } + if ($el.height) { X "$inner$($el.height)" } + Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner + Emit-Events -el $el -elementName $name -indent $inner + X "$indent" +} + +function Emit-LabelField { + param($el, [string]$name, [int]$id, [string]$indent) + X "$indent" + $inner = "$indent`t" + if ($el.path) { X "$inner$($el.path)" } + Emit-Title -el $el -name $name -indent $inner + Emit-CommonFlags -el $el -indent $inner + if ($el.hyperlink -eq $true) { X "$innertrue" } + Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner + Emit-Events -el $el -elementName $name -indent $inner + X "$indent" +} + +function Emit-Table { + param($el, [string]$name, [int]$id, [string]$indent) + X "$indent" + $inner = "$indent`t" + if ($el.path) { X "$inner$($el.path)" } + Emit-Title -el $el -name $name -indent $inner + Emit-CommonFlags -el $el -indent $inner + if ($el.representation) { X "$inner$($el.representation)" } + if ($el.changeRowSet -eq $true) { X "$innertrue" } + if ($el.changeRowOrder -eq $true) { X "$innertrue" } + if ($el.height) { X "$inner$($el.height)" } + if ($el.header -eq $false) { X "$inner
false
" } + if ($el.footer -eq $true) { X "$inner
true
" } + if ($el.commandBarLocation) { X "$inner$($el.commandBarLocation)" } + if ($el.searchStringLocation) { X "$inner$($el.searchStringLocation)" } + Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner + Emit-Companion -tag "AutoCommandBar" -name "${name}КоманднаяПанель" -indent $inner + Emit-Companion -tag "SearchStringAddition" -name "${name}СтрокаПоиска" -indent $inner + Emit-Companion -tag "ViewStatusAddition" -name "${name}СостояниеПросмотра" -indent $inner + Emit-Companion -tag "SearchControlAddition" -name "${name}УправлениеПоиском" -indent $inner + if ($el.columns -and $el.columns.Count -gt 0) { + X "$inner" + foreach ($col in $el.columns) { Emit-Element -el $col -indent "$inner`t" } + X "$inner" + } + Emit-Events -el $el -elementName $name -indent $inner + X "$indent
" +} + +function Emit-Pages { + param($el, [string]$name, [int]$id, [string]$indent) + X "$indent" + $inner = "$indent`t" + if ($el.pagesRepresentation) { X "$inner$($el.pagesRepresentation)" } + Emit-CommonFlags -el $el -indent $inner + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner + Emit-Events -el $el -elementName $name -indent $inner + if ($el.children -and $el.children.Count -gt 0) { + X "$inner" + foreach ($child in $el.children) { Emit-Element -el $child -indent "$inner`t" } + X "$inner" + } + X "$indent" +} + +function Emit-Page { + param($el, [string]$name, [int]$id, [string]$indent) + X "$indent" + $inner = "$indent`t" + Emit-Title -el $el -name $name -indent $inner + Emit-CommonFlags -el $el -indent $inner + if ($el.group) { + $orientation = switch ("$($el.group)") { "horizontal" { "Horizontal" } "vertical" { "Vertical" } "alwaysHorizontal" { "AlwaysHorizontal" } "alwaysVertical" { "AlwaysVertical" } default { $null } } + if ($orientation) { X "$inner$orientation" } + } + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner + if ($el.children -and $el.children.Count -gt 0) { + X "$inner" + foreach ($child in $el.children) { Emit-Element -el $child -indent "$inner`t" } + X "$inner" + } + X "$indent" +} + +function Emit-Button { + param($el, [string]$name, [int]$id, [string]$indent) + X "$indent" +} + +function Emit-PictureDecoration { + param($el, [string]$name, [int]$id, [string]$indent) + X "$indent" + $inner = "$indent`t" + Emit-Title -el $el -name $name -indent $inner + Emit-CommonFlags -el $el -indent $inner + if ($el.picture -or $el.src) { + $ref = if ($el.src) { "$($el.src)" } else { "$($el.picture)" } + X "$inner"; X "$inner`t$ref"; X "$inner`ttrue"; X "$inner" + } + if ($el.hyperlink -eq $true) { X "$innertrue" } + if ($el.width) { X "$inner$($el.width)" } + if ($el.height) { X "$inner$($el.height)" } + Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner + Emit-Events -el $el -elementName $name -indent $inner + X "$indent" +} + +function Emit-PictureField { + param($el, [string]$name, [int]$id, [string]$indent) + X "$indent" + $inner = "$indent`t" + if ($el.path) { X "$inner$($el.path)" } + Emit-Title -el $el -name $name -indent $inner + Emit-CommonFlags -el $el -indent $inner + if ($el.width) { X "$inner$($el.width)" } + if ($el.height) { X "$inner$($el.height)" } + Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner + Emit-Events -el $el -elementName $name -indent $inner + X "$indent" +} + +function Emit-Calendar { + param($el, [string]$name, [int]$id, [string]$indent) + X "$indent" + $inner = "$indent`t" + if ($el.path) { X "$inner$($el.path)" } + Emit-Title -el $el -name $name -indent $inner + Emit-CommonFlags -el $el -indent $inner + Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner + Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner + Emit-Events -el $el -elementName $name -indent $inner + X "$indent" +} + +function Emit-CommandBarEl { + param($el, [string]$name, [int]$id, [string]$indent) + X "$indent" + $inner = "$indent`t" + if ($el.autofill -eq $true) { X "$innertrue" } + Emit-CommonFlags -el $el -indent $inner + if ($el.children -and $el.children.Count -gt 0) { + X "$inner" + foreach ($child in $el.children) { Emit-Element -el $child -indent "$inner`t" } + X "$inner" + } + X "$indent" +} + +function Emit-Popup { + param($el, [string]$name, [int]$id, [string]$indent) + X "$indent" + $inner = "$indent`t" + Emit-Title -el $el -name $name -indent $inner + Emit-CommonFlags -el $el -indent $inner + if ($el.picture) { + X "$inner"; X "$inner`t$($el.picture)"; X "$inner`ttrue"; X "$inner" + } + if ($el.representation) { X "$inner$($el.representation)" } + if ($el.children -and $el.children.Count -gt 0) { + X "$inner" + foreach ($child in $el.children) { Emit-Element -el $child -indent "$inner`t" } + X "$inner" + } + X "$indent" +} + +# --- Element dispatcher --- + +function Emit-Element { + param($el, [string]$indent) + + $typeKey = $null + foreach ($key in @("group","input","check","label","labelField","table","pages","page","button","picture","picField","calendar","cmdBar","popup")) { + if ($el.$key -ne $null) { $typeKey = $key; break } + } + if (-not $typeKey) { Write-Warning "Unknown element type, skipping"; return } + + $name = Get-ElementName -el $el -typeKey $typeKey + $id = New-Id + + switch ($typeKey) { + "group" { Emit-Group -el $el -name $name -id $id -indent $indent } + "input" { Emit-Input -el $el -name $name -id $id -indent $indent } + "check" { Emit-Check -el $el -name $name -id $id -indent $indent } + "label" { Emit-Label -el $el -name $name -id $id -indent $indent } + "labelField" { Emit-LabelField -el $el -name $name -id $id -indent $indent } + "table" { Emit-Table -el $el -name $name -id $id -indent $indent } + "pages" { Emit-Pages -el $el -name $name -id $id -indent $indent } + "page" { Emit-Page -el $el -name $name -id $id -indent $indent } + "button" { Emit-Button -el $el -name $name -id $id -indent $indent } + "picture" { Emit-PictureDecoration -el $el -name $name -id $id -indent $indent } + "picField" { Emit-PictureField -el $el -name $name -id $id -indent $indent } + "calendar" { Emit-Calendar -el $el -name $name -id $id -indent $indent } + "cmdBar" { Emit-CommandBarEl -el $el -name $name -id $id -indent $indent } + "popup" { Emit-Popup -el $el -name $name -id $id -indent $indent } + } +} + +# === 6. Find element by name recursively === + +function Find-Element($startNode, [string]$targetName) { + foreach ($child in $startNode.ChildNodes) { + if ($child.NodeType -ne 'Element') { continue } + $childName = $child.GetAttribute("name") + if ($childName -eq $targetName) { return $child } + $ci = $child.SelectSingleNode("f:ChildItems", $nsMgr) + if ($ci) { + $found = Find-Element $ci $targetName + if ($found) { return $found } + } + } + return $null +} + +# === 7. Detect indent level of a container's children === + +function Get-ChildIndent($container) { + foreach ($child in $container.ChildNodes) { + if ($child.NodeType -eq 'Whitespace' -or $child.NodeType -eq 'SignificantWhitespace') { + $text = $child.Value + if ($text -match '^\r?\n(\t+)$') { return $Matches[1] } + if ($text -match '^\r?\n(\t+)') { return $Matches[1] } + } + } + # Fallback: count depth from root + $depth = 0 + $current = $container + while ($current -and $current -ne $xmlDoc.DocumentElement) { + $depth++ + $current = $current.ParentNode + } + return "`t" * ($depth + 1) +} + +# === 8. Insert node into container === + +function Insert-IntoContainer($container, $newNode, $afterName, $childIndent) { + $refNode = $null + + if ($afterName) { + # Find the after-element, then insert after it + $afterElem = $null + foreach ($child in $container.ChildNodes) { + if ($child.NodeType -eq 'Element' -and $child.GetAttribute("name") -eq $afterName) { + $afterElem = $child + break + } + } + if ($afterElem) { + $refNode = $afterElem.NextSibling + } else { + Write-Host "[WARN] Element '$afterName' not found in target container, appending at end" + } + } + + if (-not $refNode) { + # Append at end: insert before trailing whitespace + $trailing = $container.LastChild + if ($trailing -and ($trailing.NodeType -eq 'Whitespace' -or $trailing.NodeType -eq 'SignificantWhitespace')) { + $refNode = $trailing + } + } + + $ws = $xmlDoc.CreateWhitespace("`r`n$childIndent") + if ($refNode) { + $container.InsertBefore($ws, $refNode) | Out-Null + $container.InsertBefore($newNode, $refNode) | Out-Null + } else { + # Container is empty (self-closing) — add framing whitespace + $container.AppendChild($ws) | Out-Null + $container.AppendChild($newNode) | Out-Null + $parentIndent = if ($childIndent.Length -gt 1) { $childIndent.Substring(0, $childIndent.Length - 1) } else { "" } + $closeWs = $xmlDoc.CreateWhitespace("`r`n$parentIndent") + $container.AppendChild($closeWs) | Out-Null + } +} + +# === 9. Generate fragment, parse, import nodes === + +$allNsDecl = 'xmlns="http://v8.1c.ru/8.3/xcf/logform" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcssch="http://v8.1c.ru/8.1/data-composition-system/schema"' + +function Parse-Fragment([string]$xmlText) { + $fragDoc = New-Object System.Xml.XmlDocument + $fragDoc.PreserveWhitespace = $true + $fragDoc.LoadXml($xmlText) + return $fragDoc +} + +function Import-ElementNodes($fragDoc) { + $nodes = @() + foreach ($child in $fragDoc.DocumentElement.ChildNodes) { + if ($child.NodeType -eq 'Element') { + $nodes += $xmlDoc.ImportNode($child, $true) + } + } + return $nodes +} + +# === 10. Add elements === + +$addedElems = @() +$companionCount = 0 + +if ($def.elements -and $def.elements.Count -gt 0) { + # Resolve target container + $targetCI = $null + $intoName = if ($def.into) { "$($def.into)" } else { $null } + $afterName = if ($def.after) { "$($def.after)" } else { $null } + + if ($intoName) { + $targetGroup = Find-Element $rootCI $intoName + if (-not $targetGroup) { + Write-Host "[ERROR] Target group '$intoName' not found" + exit 1 + } + $targetCI = $targetGroup.SelectSingleNode("f:ChildItems", $nsMgr) + if (-not $targetCI) { + # Create ChildItems for the group + $targetCI = $xmlDoc.CreateElement("ChildItems", $formNs) + $targetGroup.AppendChild($targetCI) | Out-Null + } + } elseif ($afterName) { + # Find the after element globally and use its parent as target + $afterElem = Find-Element $rootCI $afterName + if (-not $afterElem) { + Write-Host "[ERROR] Element '$afterName' not found" + exit 1 + } + $targetCI = $afterElem.ParentNode + } else { + $targetCI = $rootCI + } + + if (-not $targetCI) { + # Create ChildItems section in form — insert after Events or AutoCommandBar + $targetCI = $xmlDoc.CreateElement("ChildItems", $formNs) + $insertAfter = $root.SelectSingleNode("f:Events", $nsMgr) + if (-not $insertAfter) { $insertAfter = $root.SelectSingleNode("f:AutoCommandBar", $nsMgr) } + if ($insertAfter) { + $refNode = $insertAfter.NextSibling + $ws = $xmlDoc.CreateWhitespace("`r`n`t") + $root.InsertBefore($ws, $refNode) | Out-Null + $root.InsertBefore($targetCI, $refNode) | Out-Null + } else { + $root.AppendChild($xmlDoc.CreateWhitespace("`r`n`t")) | Out-Null + $root.AppendChild($targetCI) | Out-Null + } + # Also update $rootCI reference + $rootCI = $targetCI + } + + # Detect indent level + $childIndent = Get-ChildIndent $targetCI + + # Remember starting element ID for companion counting + $startElemId = $script:nextElemId + + # Generate fragment + $script:xml = New-Object System.Text.StringBuilder 4096 + X "<_F $allNsDecl>" + foreach ($el in $def.elements) { + Emit-Element -el $el -indent $childIndent + } + X "" + + $fragDoc = Parse-Fragment $script:xml.ToString() + $importedNodes = Import-ElementNodes $fragDoc + + # Count actual elements (non-companion) for reporting + foreach ($el in $def.elements) { + $typeKey = $null + foreach ($key in @("group","input","check","label","labelField","table","pages","page","button","picture","picField","calendar","cmdBar","popup")) { + if ($el.$key -ne $null) { $typeKey = $key; break } + } + $name = Get-ElementName -el $el -typeKey $typeKey + $tagMap = @{ + "group"="Group"; "input"="Input"; "check"="Check"; "label"="Label"; "labelField"="LabelField" + "table"="Table"; "pages"="Pages"; "page"="Page"; "button"="Button" + "picture"="Picture"; "picField"="PicField"; "calendar"="Calendar"; "cmdBar"="CmdBar"; "popup"="Popup" + } + $pathStr = if ($el.path) { " -> $($el.path)" } else { "" } + $evtStr = if ($el.on) { " {$($el.on -join ', ')}" } else { "" } + $addedElems += " + [$($tagMap[$typeKey])] $name$pathStr$evtStr" + } + + # Insert each imported node + foreach ($node in $importedNodes) { + Insert-IntoContainer -container $targetCI -newNode $node -afterName $afterName -childIndent $childIndent + # Only use afterName for the first insertion; subsequent ones go after the previous + $afterName = $node.GetAttribute("name") + } + + $totalNewElemIds = $script:nextElemId - $startElemId + $companionCount = $totalNewElemIds - $def.elements.Count +} + +# === 11. Add attributes === + +$addedAttrs = @() + +if ($def.attributes -and $def.attributes.Count -gt 0) { + $attrsSection = $root.SelectSingleNode("f:Attributes", $nsMgr) + if (-not $attrsSection) { + # Create Attributes section — insert after ChildItems or after Events + $attrsSection = $xmlDoc.CreateElement("Attributes", $formNs) + # Find insertion point: after ChildItems or after the last pre-Attributes element + $insertAfter = $rootCI + if (-not $insertAfter) { + $insertAfter = $root.SelectSingleNode("f:Events", $nsMgr) + } + if (-not $insertAfter) { + $insertAfter = $root.SelectSingleNode("f:AutoCommandBar", $nsMgr) + } + if ($insertAfter) { + $refNode = $insertAfter.NextSibling + $ws = $xmlDoc.CreateWhitespace("`r`n`t") + $root.InsertBefore($ws, $refNode) | Out-Null + $root.InsertBefore($attrsSection, $refNode) | Out-Null + } else { + $root.AppendChild($xmlDoc.CreateWhitespace("`r`n`t")) | Out-Null + $root.AppendChild($attrsSection) | Out-Null + } + } + + # Detect indent for attribute children + $attrChildIndent = Get-ChildIndent $attrsSection + if (-not $attrChildIndent -or $attrChildIndent -eq "") { $attrChildIndent = "`t`t" } + + # Generate attribute fragments + $script:xml = New-Object System.Text.StringBuilder 2048 + X "<_F $allNsDecl>" + foreach ($attr in $def.attributes) { + $attrId = New-AttrId + $attrName = "$($attr.name)" + X "$attrChildIndent" + $inner = "$attrChildIndent`t" + + if ($attr.title) { Emit-MLText -tag "Title" -text "$($attr.title)" -indent $inner } + if ($attr.type) { Emit-Type -typeStr "$($attr.type)" -indent $inner } else { X "$inner" } + if ($attr.main -eq $true) { X "$innertrue" } + if ($attr.savedData -eq $true) { X "$innertrue" } + if ($attr.fillChecking) { X "$inner$($attr.fillChecking)" } + + if ($attr.columns -and $attr.columns.Count -gt 0) { + X "$inner" + $colId = 1 + foreach ($col in $attr.columns) { + X "$inner`t" + if ($col.title) { Emit-MLText -tag "Title" -text "$($col.title)" -indent "$inner`t`t" } + Emit-Type -typeStr "$($col.type)" -indent "$inner`t`t" + X "$inner`t" + $colId++ + } + X "$inner" + } + + X "$attrChildIndent" + $typeStr = if ($attr.type) { "$($attr.type)" } else { "(no type)" } + $addedAttrs += " + ${attrName}: $typeStr (id=$attrId)" + } + X "" + + $fragDoc = Parse-Fragment $script:xml.ToString() + $importedAttrs = Import-ElementNodes $fragDoc + + foreach ($node in $importedAttrs) { + Insert-IntoContainer -container $attrsSection -newNode $node -afterName $null -childIndent $attrChildIndent + } +} + +# === 12. Add commands === + +$addedCmds = @() + +if ($def.commands -and $def.commands.Count -gt 0) { + $cmdsSection = $root.SelectSingleNode("f:Commands", $nsMgr) + if (-not $cmdsSection) { + # Create Commands section — insert after Parameters or Attributes + $cmdsSection = $xmlDoc.CreateElement("Commands", $formNs) + $insertAfter = $root.SelectSingleNode("f:Parameters", $nsMgr) + if (-not $insertAfter) { $insertAfter = $root.SelectSingleNode("f:Attributes", $nsMgr) } + if (-not $insertAfter) { $insertAfter = $rootCI } + if ($insertAfter) { + $refNode = $insertAfter.NextSibling + $ws = $xmlDoc.CreateWhitespace("`r`n`t") + $root.InsertBefore($ws, $refNode) | Out-Null + $root.InsertBefore($cmdsSection, $refNode) | Out-Null + } else { + $root.AppendChild($xmlDoc.CreateWhitespace("`r`n`t")) | Out-Null + $root.AppendChild($cmdsSection) | Out-Null + } + } + + $cmdChildIndent = Get-ChildIndent $cmdsSection + if (-not $cmdChildIndent -or $cmdChildIndent -eq "") { $cmdChildIndent = "`t`t" } + + # Generate command fragments + $script:xml = New-Object System.Text.StringBuilder 1024 + X "<_F $allNsDecl>" + foreach ($cmd in $def.commands) { + $cmdId = New-CmdId + $cmdName = "$($cmd.name)" + X "$cmdChildIndent" + $inner = "$cmdChildIndent`t" + + if ($cmd.title) { Emit-MLText -tag "Title" -text "$($cmd.title)" -indent $inner } + if ($cmd.action) { X "$inner$($cmd.action)" } + if ($cmd.shortcut) { X "$inner$($cmd.shortcut)" } + if ($cmd.picture) { + X "$inner" + X "$inner`t$($cmd.picture)" + X "$inner`ttrue" + X "$inner" + } + if ($cmd.representation) { X "$inner$($cmd.representation)" } + + X "$cmdChildIndent" + $actionStr = if ($cmd.action) { " -> $($cmd.action)" } else { "" } + $addedCmds += " + ${cmdName}${actionStr} (id=$cmdId)" + } + X "" + + $fragDoc = Parse-Fragment $script:xml.ToString() + $importedCmds = Import-ElementNodes $fragDoc + + foreach ($node in $importedCmds) { + Insert-IntoContainer -container $cmdsSection -newNode $node -afterName $null -childIndent $cmdChildIndent + } +} + +# === 13. Save === + +$content = $xmlDoc.OuterXml +# Ensure encoding declaration is uppercase UTF-8 +$content = $content -replace '^<\?xml version="1.0" encoding="utf-8"\?>', '' + +$enc = New-Object System.Text.UTF8Encoding($true) +[System.IO.File]::WriteAllText($resolvedFormPath, $content, $enc) + +# === 14. Summary === + +if ($addedElems.Count -gt 0) { + $posStr = "" + if ($def.into) { $posStr += "into $($def.into)" } + if ($def.after) { if ($posStr) { $posStr += ", " }; $posStr += "after $($def.after)" } + if ($posStr) { $posStr = " ($posStr)" } + Write-Host "Added elements${posStr}:" + foreach ($line in $addedElems) { Write-Host $line } + Write-Host "" +} + +if ($addedAttrs.Count -gt 0) { + Write-Host "Added attributes:" + foreach ($line in $addedAttrs) { Write-Host $line } + Write-Host "" +} + +if ($addedCmds.Count -gt 0) { + Write-Host "Added commands:" + foreach ($line in $addedCmds) { Write-Host $line } + Write-Host "" +} + +Write-Host "---" +$totalParts = @() +if ($addedElems.Count -gt 0) { + $compStr = if ($companionCount -gt 0) { " (+$companionCount companions)" } else { "" } + $totalParts += "$($addedElems.Count) element(s)$compStr" +} +if ($addedAttrs.Count -gt 0) { $totalParts += "$($addedAttrs.Count) attribute(s)" } +if ($addedCmds.Count -gt 0) { $totalParts += "$($addedCmds.Count) command(s)" } +Write-Host "Total: $($totalParts -join ', ')" +Write-Host "Run /form-validate to verify." diff --git a/README.md b/README.md index 65c70c55..2cb9d208 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ |--------|--------|----------|------| | Внешние обработки (EPF) | 10 навыков `/epf-*` | Создание, модификация, сборка обработок из XML-исходников | [Подробнее](docs/epf-guide.md) | | Табличный документ (MXL) | 4 навыка `/mxl-*` | Анализ, создание, компиляция макетов печатных форм | [Подробнее](docs/mxl-guide.md) | -| Управляемые формы (Form) | 3 навыка `/form-*` | Анализ, генерация, валидация управляемых форм | [Подробнее](docs/form-guide.md) | +| Управляемые формы (Form) | 4 навыка `/form-*` | Анализ, генерация, модификация, валидация управляемых форм | [Подробнее](docs/form-guide.md) | | Утилиты | `/img-grid` | Наложение сетки на изображение для определения пропорций колонок | — | ## Требования @@ -60,6 +60,7 @@ ├── form-info/ # Анализ структуры управляемой формы ├── form-compile/ # Компиляция формы из JSON ├── form-validate/ # Валидация формы +├── form-add/ # Добавление элементов в форму └── img-grid/ # Сетка для анализа изображений docs/ ├── epf-guide.md # Гайд: внешние обработки diff --git a/docs/form-guide.md b/docs/form-guide.md index f0d8087c..46d02958 100644 --- a/docs/form-guide.md +++ b/docs/form-guide.md @@ -9,6 +9,7 @@ | `/form-info` | `` | Компактная сводка: дерево элементов, реквизиты, команды, события | | `/form-compile` | ` ` | Генерация Form.xml из компактного JSON-определения | | `/form-validate` | `` | Валидация: уникальность ID, companions, DataPath, команды | +| `/form-add` | ` ` | Добавление элементов, реквизитов, команд в существующую форму | ## Сценарии использования @@ -51,15 +52,32 @@ Commands: Печать -> ПечатьДокумента [Ctrl+P] ``` -### Добавление элемента на форму +### Добавление элементов в существующую форму -Чтобы добавить поле на форму, нужно знать структуру групп и ориентацию (горизонтальная/вертикальная). `/form-info` показывает это в дереве элементов. +`/form-add` добавляет элементы, реквизиты и команды в существующий Form.xml. Автоматически назначает ID, генерирует companion-элементы и обработчики событий. ``` -> Добавь поле "Склад" в шапку формы документа +> Добавь поле "Склад" в шапку формы после "Контрагент" ``` -Claude вызовет `/form-info`, увидит `[Group:AH] ГруппаШапка` → `[Group:V] ГруппаШапкаЛевая`, поймёт куда вставить элемент в XML. +Claude вызовет `/form-info` для анализа структуры, создаст JSON и вызовет `/form-add`: + +```json +{ + "into": "ГруппаШапка", + "after": "Контрагент", + "elements": [ + { "input": "Склад", "path": "Объект.Склад", "on": ["OnChange"] } + ], + "attributes": [ + { "name": "Склад", "type": "CatalogRef.Склады" } + ] +} +``` + +Позиционирование: `into` указывает группу-контейнер, `after` — элемент, после которого вставлять. Оба опциональны (по умолчанию — в конец корневого ChildItems). + +Один вызов может содержать элементы, реквизиты и команды одновременно. Группы поддерживают `children` для вложенных элементов. ### Поиск обработчиков и привязок @@ -173,6 +191,7 @@ Claude создаст JSON-определение и вызовет `/form-compi > /form-info upload/acc_8.3.24/Documents/РеализацияТоваровУслуг/Forms/ФормаДокумента/Ext/Form.xml > /form-info src/МояОбработка/Forms/Форма/Ext/Form.xml > /form-compile src/form.json src/МояОбработка/Forms/Форма/Ext/Form.xml +> /form-add src/МояОбработка/Forms/Форма/Ext/Form.xml src/additions.json > /form-validate src/МояОбработка/Forms/Форма/Ext/Form.xml ``` @@ -182,11 +201,12 @@ Claude создаст JSON-определение и вызовет `/form-compi 1. `/epf-add-form` — создать форму (каркас) 2. `/form-compile` — сгенерировать Form.xml из JSON-определения -3. `/form-validate` — проверить корректность -4. `/form-info` — проанализировать результат -5. `/epf-build` — собрать EPF +3. `/form-add` — добавить элементы/реквизиты/команды в существующую форму +4. `/form-validate` — проверить корректность +5. `/form-info` — проанализировать результат +6. `/epf-build` — собрать EPF ## Спецификации - [Управляемая форма](1c-form-spec.md) — Form.xml, элементы, команды, реквизиты, система типов -- [Form DSL](form-dsl-spec.md) — JSON-формат описания формы для `/form-compile` +- [Form DSL](form-dsl-spec.md) — JSON-формат описания формы для `/form-compile` и `/form-add` From a9e59265dd3cbdcdb4ad2b19b37584626ae63a33 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Mon, 9 Feb 2026 14:40:23 +0300 Subject: [PATCH 05/13] Fix ID scanning in form-add and title emission in form-compile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit form-add: Scan column IDs (same pool as attribute IDs) to prevent duplicate ID collisions. Use XPath .//*[@id] instead of ChildItems recursion to capture companion element IDs (ExtendedTooltip, ContextMenu). form-compile: Extract title from properties and route through Emit-MLText instead of generic Emit-Properties, which produced plain text rejected by 1C XDTO schema. Found during E2E testing of full pipeline (epf-init → form-compile → form-add → epf-build). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --- .claude/skills/form-add/scripts/form-add.ps1 | 44 +++++++++---------- .../form-compile/scripts/form-compile.ps1 | 24 +++++++--- 2 files changed, 41 insertions(+), 27 deletions(-) diff --git a/.claude/skills/form-add/scripts/form-add.ps1 b/.claude/skills/form-add/scripts/form-add.ps1 index 7f73b800..53a0527c 100644 --- a/.claude/skills/form-add/scripts/form-add.ps1 +++ b/.claude/skills/form-add/scripts/form-add.ps1 @@ -63,37 +63,37 @@ $script:nextElemId = 0 $script:nextAttrId = 0 $script:nextCmdId = 0 -function Scan-ElementIds($node) { - foreach ($child in $node.ChildNodes) { - if ($child.NodeType -ne 'Element') { continue } - $id = $child.GetAttribute("id") - if ($id -and $id -ne "" -and $id -ne "-1") { - try { - $intId = [int]$id - if ($intId -gt $script:nextElemId) { $script:nextElemId = $intId } - } catch {} +# Scan ALL element IDs via XPath (includes companions like ExtendedTooltip, ContextMenu) +$rootCI = $root.SelectSingleNode("f:ChildItems", $nsMgr) +if ($rootCI) { + foreach ($elem in $rootCI.SelectNodes(".//*[@id]")) { + $id = $elem.GetAttribute("id") + if ($id -and $id -ne "-1") { + try { $intId = [int]$id; if ($intId -gt $script:nextElemId) { $script:nextElemId = $intId } } catch {} } - $ci = $child.SelectSingleNode("f:ChildItems", $nsMgr) - if ($ci) { Scan-ElementIds $ci } + } +} +$acb = $root.SelectSingleNode("f:AutoCommandBar", $nsMgr) +if ($acb) { + $id = $acb.GetAttribute("id") + if ($id -and $id -ne "-1") { + try { $intId = [int]$id; if ($intId -gt $script:nextElemId) { $script:nextElemId = $intId } } catch {} } } -$rootCI = $root.SelectSingleNode("f:ChildItems", $nsMgr) -if ($rootCI) { Scan-ElementIds $rootCI } - -# Also scan AutoCommandBar children -$acb = $root.SelectSingleNode("f:AutoCommandBar", $nsMgr) -if ($acb) { - $acbCI = $acb.SelectSingleNode("f:ChildItems", $nsMgr) - if ($acbCI) { Scan-ElementIds $acbCI } -} - -# Scan attribute IDs +# Scan attribute IDs (including column IDs — same pool) foreach ($attr in $root.SelectNodes("f:Attributes/f:Attribute", $nsMgr)) { $id = $attr.GetAttribute("id") if ($id) { try { $intId = [int]$id; if ($intId -gt $script:nextAttrId) { $script:nextAttrId = $intId } } catch {} } + # Column IDs are in the same pool as attribute IDs + foreach ($col in $attr.SelectNodes("f:Columns/f:Column", $nsMgr)) { + $colId = $col.GetAttribute("id") + if ($colId) { + try { $intColId = [int]$colId; if ($intColId -gt $script:nextAttrId) { $script:nextAttrId = $intColId } } catch {} + } + } } # Scan command IDs diff --git a/.claude/skills/form-compile/scripts/form-compile.ps1 b/.claude/skills/form-compile/scripts/form-compile.ps1 index c10d1acf..bb5bb426 100644 --- a/.claude/skills/form-compile/scripts/form-compile.ps1 +++ b/.claude/skills/form-compile/scripts/form-compile.ps1 @@ -978,13 +978,27 @@ $script:nextId = 1 X '<?xml version="1.0" encoding="UTF-8"?>' X '<Form xmlns="http://v8.1c.ru/8.3/xcf/logform" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcssch="http://v8.1c.ru/8.1/data-composition-system/schema" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">' -# 12a. Title -if ($def.title) { - Emit-MLText -tag "Title" -text "$($def.title)" -indent "`t" +# 12a. Title (from def.title or properties.title — must be multilingual XML) +$formTitle = $def.title +if (-not $formTitle -and $def.properties -and $def.properties.title) { + $formTitle = $def.properties.title +} +if ($formTitle) { + Emit-MLText -tag "Title" -text "$formTitle" -indent "`t" } -# 12b. Properties -Emit-Properties -props $def.properties -indent "`t" +# 12b. Properties (skip 'title' — handled above as multilingual) +if ($def.properties) { + $propsClone = New-Object PSObject + foreach ($p in $def.properties.PSObject.Properties) { + if ($p.Name -ne "title") { + $propsClone | Add-Member -NotePropertyName $p.Name -NotePropertyValue $p.Value + } + } + Emit-Properties -props $propsClone -indent "`t" +} else { + Emit-Properties -props $null -indent "`t" +} # 12c. CommandSet (excluded commands) if ($def.excludedCommands -and $def.excludedCommands.Count -gt 0) { From d05199d048112024cc9157fa9820406b99253135 Mon Sep 17 00:00:00 2001 From: Nick Shirokov <nick_4312@mail.ru> Date: Mon, 9 Feb 2026 15:05:14 +0300 Subject: [PATCH 06/13] Replace hidden/disabled with visible/enabled in form DSL Rename DSL keys to match 1C property names (Visible, Enabled): - form-compile/form-add: accept "visible": false and "enabled": false as primary keys, keep hidden/disabled as synonyms - form-info: output [visible:false] and [enabled:false] flags - Update SKILL.md docs and form-guide.md Improves round-trip consistency: form-info output now directly maps to form-compile/form-add input keys. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --- .claude/skills/form-add/scripts/form-add.ps1 | 4 ++-- .claude/skills/form-compile/SKILL.md | 4 ++-- .claude/skills/form-compile/scripts/form-compile.ps1 | 4 ++-- .claude/skills/form-info/SKILL.md | 6 +++--- .claude/skills/form-info/scripts/form-info.ps1 | 4 ++-- docs/form-guide.md | 4 ++-- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.claude/skills/form-add/scripts/form-add.ps1 b/.claude/skills/form-add/scripts/form-add.ps1 index 53a0527c..ad6ee781 100644 --- a/.claude/skills/form-add/scripts/form-add.ps1 +++ b/.claude/skills/form-add/scripts/form-add.ps1 @@ -254,8 +254,8 @@ function Emit-Companion { function Emit-CommonFlags { param($el, [string]$indent) - if ($el.hidden -eq $true) { X "$indent<Visible>false</Visible>" } - if ($el.disabled -eq $true) { X "$indent<Enabled>false</Enabled>" } + if ($el.visible -eq $false -or $el.hidden -eq $true) { X "$indent<Visible>false</Visible>" } + if ($el.enabled -eq $false -or $el.disabled -eq $true) { X "$indent<Enabled>false</Enabled>" } if ($el.readOnly -eq $true) { X "$indent<ReadOnly>true</ReadOnly>" } } diff --git a/.claude/skills/form-compile/SKILL.md b/.claude/skills/form-compile/SKILL.md index 90b9b59c..6791f941 100644 --- a/.claude/skills/form-compile/SKILL.md +++ b/.claude/skills/form-compile/SKILL.md @@ -58,8 +58,8 @@ powershell.exe -NoProfile -File .claude\skills\form-compile\scripts\form-compile - `"name"` — переопределить имя (по умолчанию из значения ключа типа) - `"path"` — DataPath (привязка к данным) - `"title"` — заголовок -- `"hidden": true` — Visible=false -- `"disabled": true` — Enabled=false +- `"visible": false` — Visible=false (скрыть элемент) +- `"enabled": false` — Enabled=false (сделать недоступным) - `"readOnly": true` — ReadOnly=true - `"on": ["OnChange", "StartChoice"]` — события с автоименованием обработчиков diff --git a/.claude/skills/form-compile/scripts/form-compile.ps1 b/.claude/skills/form-compile/scripts/form-compile.ps1 index bb5bb426..d7bc8f13 100644 --- a/.claude/skills/form-compile/scripts/form-compile.ps1 +++ b/.claude/skills/form-compile/scripts/form-compile.ps1 @@ -295,8 +295,8 @@ function Emit-Element { function Emit-CommonFlags { param($el, [string]$indent) - if ($el.hidden -eq $true) { X "$indent<Visible>false</Visible>" } - if ($el.disabled -eq $true) { X "$indent<Enabled>false</Enabled>" } + if ($el.visible -eq $false -or $el.hidden -eq $true) { X "$indent<Visible>false</Visible>" } + if ($el.enabled -eq $false -or $el.disabled -eq $true) { X "$indent<Enabled>false</Enabled>" } if ($el.readOnly -eq $true) { X "$indent<ReadOnly>true</ReadOnly>" } } diff --git a/.claude/skills/form-info/SKILL.md b/.claude/skills/form-info/SKILL.md index eed56b7e..8f61e1f5 100644 --- a/.claude/skills/form-info/SKILL.md +++ b/.claude/skills/form-info/SKILL.md @@ -71,7 +71,7 @@ Events: Elements: ├─ [Group:AH] ГруппаШапка │ ├─ [Input] Организация -> Объект.Организация {OnChange} - │ └─ [Input] Договор -> Объект.Договор [hidden] {StartChoice} + │ └─ [Input] Договор -> Объект.Договор [visible:false] {StartChoice} ├─ [Table] Товары -> Объект.Товары │ ├─ [Input] Номенклатура -> Объект.Товары.Номенклатура {OnChange} │ └─ [Input] Сумма -> Объект.Товары.Сумма [ro] @@ -105,8 +105,8 @@ Elements: | `[BtnGroup]` | ButtonGroup | **Флаги** (только при отклонении от умолчания): -- `[hidden]` — Visible=false -- `[disabled]` — Enabled=false +- `[visible:false]` — элемент скрыт (Visible=false) +- `[enabled:false]` — элемент недоступен (Enabled=false) - `[ro]` — ReadOnly=true - `,collapse` — Behavior=Collapsible (для групп) diff --git a/.claude/skills/form-info/scripts/form-info.ps1 b/.claude/skills/form-info/scripts/form-info.ps1 index 459a51f9..12f84674 100644 --- a/.claude/skills/form-info/scripts/form-info.ps1 +++ b/.claude/skills/form-info/scripts/form-info.ps1 @@ -149,9 +149,9 @@ function Get-EventsStr($node) { function Get-Flags($node) { $flags = @() $vis = $node.SelectSingleNode("d:Visible", $ns) - if ($vis -and $vis.InnerText -eq "false") { $flags += "hidden" } + if ($vis -and $vis.InnerText -eq "false") { $flags += "visible:false" } $en = $node.SelectSingleNode("d:Enabled", $ns) - if ($en -and $en.InnerText -eq "false") { $flags += "disabled" } + if ($en -and $en.InnerText -eq "false") { $flags += "enabled:false" } $ro = $node.SelectSingleNode("d:ReadOnly", $ns) if ($ro -and $ro.InnerText -eq "true") { $flags += "ro" } if ($flags.Count -eq 0) { return "" } diff --git a/docs/form-guide.md b/docs/form-guide.md index 46d02958..34f4ed38 100644 --- a/docs/form-guide.md +++ b/docs/form-guide.md @@ -122,8 +122,8 @@ Claude сделает grep по Form.xml и найдёт полный XML-бло ### Флаги Показываются только при отклонении от умолчания: -- `[hidden]` — элемент скрыт (Visible=false) -- `[disabled]` — элемент недоступен (Enabled=false) +- `[visible:false]` — элемент скрыт (Visible=false) +- `[enabled:false]` — элемент недоступен (Enabled=false) - `[ro]` — только чтение (ReadOnly=true) - `,collapse` — сворачиваемая группа (Behavior=Collapsible) From c66580091611035986ca68f313602c44a12026db Mon Sep 17 00:00:00 2001 From: Nick Shirokov <nick_4312@mail.ru> Date: Mon, 9 Feb 2026 15:44:32 +0300 Subject: [PATCH 07/13] Improve form skills: validation, docs, round-trip consistency form-compile: warn about unknown DSL keys in element definitions, document stdCommand/command button keys and EPF-specific notes. form-validate: check that form-level Title uses multilingual XML, not plain text (which causes XDTO errors at build time). form-add: warn about duplicate element names, clarify after-not-found message when using into+after together. form-info: show Title in header instead of Properties line, display commands as DSL-friendly format (-> Name [cmd], -> Close [std]) instead of raw Form.Command/Form.StandardCommand paths. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --- .claude/skills/form-add/scripts/form-add.ps1 | 17 +++++++- .claude/skills/form-compile/SKILL.md | 13 ++++++ .../form-compile/scripts/form-compile.ps1 | 41 +++++++++++++++++++ .claude/skills/form-info/SKILL.md | 10 +++-- .../skills/form-info/scripts/form-info.ps1 | 24 +++++++++-- .../form-validate/scripts/form-validate.ps1 | 14 +++++++ 6 files changed, 110 insertions(+), 9 deletions(-) diff --git a/.claude/skills/form-add/scripts/form-add.ps1 b/.claude/skills/form-add/scripts/form-add.ps1 index ad6ee781..76ad3e28 100644 --- a/.claude/skills/form-add/scripts/form-add.ps1 +++ b/.claude/skills/form-add/scripts/form-add.ps1 @@ -634,7 +634,7 @@ function Insert-IntoContainer($container, $newNode, $afterName, $childIndent) { if ($afterElem) { $refNode = $afterElem.NextSibling } else { - Write-Host "[WARN] Element '$afterName' not found in target container, appending at end" + Write-Host "[WARN] after='$afterName' not found in target container, appending at end" } } @@ -737,6 +737,21 @@ if ($def.elements -and $def.elements.Count -gt 0) { # Detect indent level $childIndent = Get-ChildIndent $targetCI + # Check for duplicate element names + foreach ($el in $def.elements) { + $typeKey = $null + foreach ($key in @("group","input","check","label","labelField","table","pages","page","button","picture","picField","calendar","cmdBar","popup")) { + if ($el.$key -ne $null) { $typeKey = $key; break } + } + if ($typeKey) { + $elName = Get-ElementName -el $el -typeKey $typeKey + $existing = Find-Element $rootCI $elName + if ($existing) { + Write-Host "[WARN] Element '$elName' already exists in form (id=$($existing.GetAttribute('id')))" + } + } + } + # Remember starting element ID for companion counting $startElemId = $script:nextElemId diff --git a/.claude/skills/form-compile/SKILL.md b/.claude/skills/form-compile/SKILL.md index 6791f941..68011a39 100644 --- a/.claude/skills/form-compile/SKILL.md +++ b/.claude/skills/form-compile/SKILL.md @@ -63,6 +63,13 @@ powershell.exe -NoProfile -File .claude\skills\form-compile\scripts\form-compile - `"readOnly": true` — ReadOnly=true - `"on": ["OnChange", "StartChoice"]` — события с автоименованием обработчиков +### Свойства кнопок + +- `"command": "ИмяКоманды"` — привязка к команде формы → `Form.Command.ИмяКоманды` +- `"stdCommand": "Close"` — привязка к стандартной команде → `Form.StandardCommand.Close` +- `"defaultButton": true` — кнопка по умолчанию +- `"type": "hyperlink"` — тип кнопки (`"usual"` / `"hyperlink"` / `"commandBar"`) + ### Система типов (shorthand) | DSL | XML | @@ -119,3 +126,9 @@ powershell.exe -NoProfile -File .claude\skills\form-compile\scripts\form-compile Структура в сводке должна совпадать с определением в JSON. +## Особенности для внешних обработок (EPF) + +- **Тип главного реквизита**: `ExternalDataProcessorObject.ИмяОбработки` (не `DataProcessorObject`) +- **DataPath**: используйте реквизиты формы (`ИмяРеквизита`), а не `Объект.ИмяРеквизита` — у внешних обработок нет реквизитов объекта в метаданных +- **Ссылочные типы**: `CatalogRef.XXX`, `DocumentRef.XXX` и т.д. могут не собраться в пустой базе — используйте `string` или базовые типы для автономной сборки + diff --git a/.claude/skills/form-compile/scripts/form-compile.ps1 b/.claude/skills/form-compile/scripts/form-compile.ps1 index d7bc8f13..3d0adc66 100644 --- a/.claude/skills/form-compile/scripts/form-compile.ps1 +++ b/.claude/skills/form-compile/scripts/form-compile.ps1 @@ -272,6 +272,47 @@ function Emit-Element { return } + # Validate known keys — warn about typos and unknown properties + $knownKeys = @{ + # type keys + "group"=1;"input"=1;"check"=1;"label"=1;"labelField"=1;"table"=1;"pages"=1;"page"=1 + "button"=1;"picture"=1;"picField"=1;"calendar"=1;"cmdBar"=1;"popup"=1 + # naming & binding + "name"=1;"path"=1;"title"=1 + # visibility & state + "visible"=1;"hidden"=1;"enabled"=1;"disabled"=1;"readOnly"=1 + # events + "on"=1;"handlers"=1 + # layout + "titleLocation"=1;"representation"=1;"width"=1;"height"=1 + "horizontalStretch"=1;"verticalStretch"=1;"autoMaxWidth"=1;"autoMaxHeight"=1 + # input-specific + "multiLine"=1;"passwordMode"=1;"choiceButton"=1;"clearButton"=1 + "spinButton"=1;"dropListButton"=1;"markIncomplete"=1;"skipOnInput"=1;"inputHint"=1 + # label/hyperlink + "hyperlink"=1 + # group-specific + "showTitle"=1;"united"=1 + # hierarchy + "children"=1;"columns"=1 + # table-specific + "changeRowSet"=1;"changeRowOrder"=1;"header"=1;"footer"=1 + "commandBarLocation"=1;"searchStringLocation"=1 + # pages-specific + "pagesRepresentation"=1 + # button-specific + "type"=1;"command"=1;"stdCommand"=1;"defaultButton"=1;"locationInCommandBar"=1 + # picture/decoration + "src"=1 + # cmdBar-specific + "autofill"=1 + } + foreach ($p in $el.PSObject.Properties) { + if (-not $knownKeys.ContainsKey($p.Name)) { + Write-Warning "Element '$($el.$typeKey)': unknown key '$($p.Name)' — ignored. Check SKILL.md for valid keys." + } + } + $name = Get-ElementName -el $el -typeKey $typeKey $id = New-Id diff --git a/.claude/skills/form-info/SKILL.md b/.claude/skills/form-info/SKILL.md index 8f61e1f5..9a564f47 100644 --- a/.claude/skills/form-info/SKILL.md +++ b/.claude/skills/form-info/SKILL.md @@ -42,14 +42,14 @@ powershell.exe -NoProfile -File .claude\skills\form-info\scripts\form-info.ps1 - ### Заголовок ``` -=== Form: ФормаДокумента (Documents.РеализацияТоваровУслуг) === +=== Form: ФормаДокумента — "Реализация товаров и услуг" (Documents.РеализацияТоваровУслуг) === ``` -Имя формы и контекст объекта определяются из пути к файлу. +Имя формы, заголовок (Title) и контекст объекта определяются из пути к файлу и XML. ### Properties — свойства формы -Только нестандартные свойства (отличающиеся от умолчания): +Только нестандартные свойства (отличающиеся от умолчания). Title показывается в заголовке, не здесь: ``` Properties: AutoTitle=false, WindowOpeningMode=LockOwnerWindow, CommandBarLocation=Bottom @@ -110,7 +110,9 @@ Elements: - `[ro]` — ReadOnly=true - `,collapse` — Behavior=Collapsible (для групп) -**Привязка**: `-> Объект.Поле` — DataPath или CommandName +**Привязка к данным**: `-> Объект.Поле` — DataPath + +**Привязка к команде**: `-> ИмяКоманды [cmd]` — команда формы, `-> Close [std]` — стандартная команда **События**: `{OnChange, StartChoice}` — имена обработчиков diff --git a/.claude/skills/form-info/scripts/form-info.ps1 b/.claude/skills/form-info/scripts/form-info.ps1 index 12f84674..61ef72da 100644 --- a/.claude/skills/form-info/scripts/form-info.ps1 +++ b/.claude/skills/form-info/scripts/form-info.ps1 @@ -253,7 +253,16 @@ function Build-Tree($childItemsNode, [string]$prefix, [bool]$isLast) { $binding = " -> $($dp.InnerText)" } else { $cn = $child.SelectSingleNode("d:CommandName", $ns) - if ($cn) { $binding = " -> $($cn.InnerText)" } + if ($cn) { + $cnVal = $cn.InnerText + if ($cnVal -match '^Form\.StandardCommand\.(.+)$') { + $binding = " -> $($Matches[1]) [std]" + } elseif ($cnVal -match '^Form\.Command\.(.+)$') { + $binding = " -> $($Matches[1]) [cmd]" + } else { + $binding = " -> $cnVal" + } + } } # Title differs? @@ -322,16 +331,23 @@ if ($formsIdx -ge 0 -and ($formsIdx + 1) -lt $parts.Count) { $lines = @() -# Header +# Header — include Title if present +$titleNode = $root.SelectSingleNode("d:Title", $ns) +$formTitle = $null +if ($titleNode) { + $formTitle = Get-MLText $titleNode + if (-not $formTitle) { $formTitle = $titleNode.InnerText } +} $header = "=== Form: $formName" +if ($formTitle) { $header += " — `"$formTitle`"" } if ($objectContext) { $header += " ($objectContext)" } $header += " ===" $lines += $header -# --- Form properties --- +# --- Form properties (Title excluded — shown in header) --- $propNames = @( - "Title", "Width", "Height", "Group", + "Width", "Height", "Group", "WindowOpeningMode", "EnterKeyBehavior", "AutoTitle", "AutoURL", "AutoFillCheck", "Customizable", "CommandBarLocation", "SaveDataInSettings", "AutoSaveDataInSettings", diff --git a/.claude/skills/form-validate/scripts/form-validate.ps1 b/.claude/skills/form-validate/scripts/form-validate.ps1 index ca13dcdd..696dd310 100644 --- a/.claude/skills/form-validate/scripts/form-validate.ps1 +++ b/.claude/skills/form-validate/scripts/form-validate.ps1 @@ -463,6 +463,20 @@ if (-not $stopped) { } } +# --- Check 10: Title must be multilingual XML (not plain text) --- + +if (-not $stopped) { + $titleNode = $root.SelectSingleNode("f:Title", $nsMgr) + if ($titleNode) { + $v8items = $titleNode.SelectNodes("v8:item", $nsMgr) + if ($v8items.Count -eq 0 -and $titleNode.InnerText.Trim() -ne "") { + Report-Error "Form Title is plain text ('$($titleNode.InnerText.Trim())') — must be multilingual XML (<v8:item>). Use top-level 'title' key in form-compile DSL." + } else { + Report-OK "Title: multilingual XML" + } + } +} + # --- Summary --- Write-Host "" From ad412dffa2c8ef6fc88d58c4395d0ceff74fbd5c Mon Sep 17 00:00:00 2001 From: Nick Shirokov <nick_4312@mail.ru> Date: Mon, 9 Feb 2026 15:58:15 +0300 Subject: [PATCH 08/13] Expand form-compile SKILL.md with full DSL reference and patterns Replace minimal docs with comprehensive reference: - Per-element-type property tables (input, check, label, group, table, pages, button, cmdBar) with all supported keys - Top-level structure documentation (title, properties, events, etc.) - Table+attribute linkage section explaining the ValueTable pattern - Three pattern examples: file import dialog, wizard with steps, list with filter and table - Attribute and command definition examples This helps the model generate correct JSON DSL from natural language requirements on the first attempt without guessing undocumented keys. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --- .claude/skills/form-compile/SKILL.md | 310 +++++++++++++++++++++++---- 1 file changed, 273 insertions(+), 37 deletions(-) diff --git a/.claude/skills/form-compile/SKILL.md b/.claude/skills/form-compile/SKILL.md index 68011a39..3e8a6138 100644 --- a/.claude/skills/form-compile/SKILL.md +++ b/.claude/skills/form-compile/SKILL.md @@ -32,7 +32,27 @@ allowed-tools: powershell.exe -NoProfile -File .claude\skills\form-compile\scripts\form-compile.ps1 -JsonPath "<json>" -OutputPath "<xml>" ``` -## JSON DSL — краткая справка +## JSON DSL — справка + +### Структура верхнего уровня + +```json +{ + "title": "Заголовок формы", + "properties": { "autoTitle": false, ... }, + "events": { "OnCreateAtServer": "ПриСозданииНаСервере" }, + "excludedCommands": ["Reread"], + "elements": [ ... ], + "attributes": [ ... ], + "commands": [ ... ], + "parameters": [ ... ] +} +``` + +- `title` — заголовок формы (multilingual). Можно указать и в `properties`, но лучше на верхнем уровне +- `properties` — свойства формы: `autoTitle`, `windowOpeningMode`, `commandBarLocation`, `saveDataInSettings`, `width`, `height` и др. +- `events` — обработчики событий формы (ключ: имя события 1С, значение: имя процедуры) +- `excludedCommands` — исключённые стандартные команды ### Элементы (ключ определяет тип) @@ -41,7 +61,7 @@ powershell.exe -NoProfile -File .claude\skills\form-compile\scripts\form-compile | `"group"` | UsualGroup | `"horizontal"` / `"vertical"` / `"alwaysHorizontal"` / `"alwaysVertical"` / `"collapsible"` | | `"input"` | InputField | имя элемента | | `"check"` | CheckBoxField | имя | -| `"label"` | LabelDecoration | имя | +| `"label"` | LabelDecoration | имя (текст задаётся через `title`) | | `"labelField"` | LabelField | имя | | `"table"` | Table | имя | | `"pages"` | Pages | имя | @@ -53,24 +73,134 @@ powershell.exe -NoProfile -File .claude\skills\form-compile\scripts\form-compile | `"cmdBar"` | CommandBar | имя | | `"popup"` | Popup | имя | -### Общие свойства элементов +### Общие свойства (все типы элементов) -- `"name"` — переопределить имя (по умолчанию из значения ключа типа) -- `"path"` — DataPath (привязка к данным) -- `"title"` — заголовок -- `"visible": false` — Visible=false (скрыть элемент) -- `"enabled": false` — Enabled=false (сделать недоступным) -- `"readOnly": true` — ReadOnly=true -- `"on": ["OnChange", "StartChoice"]` — события с автоименованием обработчиков +| Ключ | Описание | +|------|----------| +| `name` | Переопределить имя (по умолчанию = значение ключа типа) | +| `title` | Заголовок элемента | +| `visible: false` | Скрыть (синоним: `hidden: true`) | +| `enabled: false` | Сделать недоступным (синоним: `disabled: true`) | +| `readOnly: true` | Только чтение | +| `on: [...]` | События с автоименованием обработчиков | +| `handlers: {...}` | Явное задание имён обработчиков: `{"OnChange": "МоёИмя"}` | -### Свойства кнопок +### Поле ввода (input) -- `"command": "ИмяКоманды"` — привязка к команде формы → `Form.Command.ИмяКоманды` -- `"stdCommand": "Close"` — привязка к стандартной команде → `Form.StandardCommand.Close` -- `"defaultButton": true` — кнопка по умолчанию -- `"type": "hyperlink"` — тип кнопки (`"usual"` / `"hyperlink"` / `"commandBar"`) +| Ключ | Описание | Пример | +|------|----------|--------| +| `path` | DataPath — привязка к данным | `"Объект.Организация"` | +| `titleLocation` | Размещение заголовка | `"none"`, `"left"`, `"top"` | +| `multiLine: true` | Многострочное поле | текстовое поле, комментарий | +| `passwordMode: true` | Режим пароля (звёздочки) | поле ввода пароля | +| `choiceButton: true` | Кнопка выбора ("...") | ссылочное поле | +| `clearButton: true` | Кнопка очистки ("X") | | +| `spinButton: true` | Кнопка прокрутки | числовые поля | +| `dropListButton: true` | Кнопка выпадающего списка | | +| `markIncomplete: true` | Пометка незаполненного | обязательные поля | +| `skipOnInput: true` | Пропускать при обходе Tab | | +| `inputHint` | Подсказка в пустом поле | `"Введите наименование..."` | +| `width` / `height` | Размер | числа | +| `autoMaxWidth: false` | Отключить авто-ширину | для фиксированных полей | +| `horizontalStretch: true` | Растягивать по ширине | | -### Система типов (shorthand) +### Чекбокс (check) + +| Ключ | Описание | +|------|----------| +| `path` | DataPath | +| `titleLocation` | Размещение заголовка | + +### Надпись-декорация (label) + +| Ключ | Описание | +|------|----------| +| `title` | Текст надписи (обязательно) | +| `hyperlink: true` | Сделать ссылкой | +| `width` / `height` | Размер | + +### Группа (group) + +Значение ключа задаёт ориентацию: `"horizontal"`, `"vertical"`, `"alwaysHorizontal"`, `"alwaysVertical"`, `"collapsible"`. + +| Ключ | Описание | +|------|----------| +| `showTitle: true` | Показывать заголовок группы | +| `united: false` | Не объединять рамку | +| `representation` | `"none"`, `"normal"`, `"weak"`, `"strong"` | +| `children: [...]` | Вложенные элементы | + +### Таблица (table) + +**Важно**: таблица требует связанный реквизит формы типа `ValueTable` с колонками (см. раздел "Связки"). + +| Ключ | Описание | +|------|----------| +| `path` | DataPath (привязка к реквизиту-таблице) | +| `columns: [...]` | Колонки — массив элементов (обычно `input`) | +| `changeRowSet: true` | Разрешить добавление/удаление строк | +| `changeRowOrder: true` | Разрешить перемещение строк | +| `height` | Высота в строках таблицы | +| `header: false` | Скрыть шапку | +| `footer: true` | Показать подвал | +| `commandBarLocation` | `"None"`, `"Top"`, `"Auto"` | +| `searchStringLocation` | `"None"`, `"Top"`, `"Auto"` | + +### Страницы (pages + page) + +| Ключ (pages) | Описание | +|------|----------| +| `pagesRepresentation` | `"None"`, `"TabsOnTop"`, `"TabsOnBottom"` и др. | +| `children: [...]` | Массив `page` | + +| Ключ (page) | Описание | +|------|----------| +| `title` | Заголовок вкладки | +| `group` | Ориентация внутри страницы | +| `children: [...]` | Содержимое страницы | + +### Кнопка (button) + +| Ключ | Описание | +|------|----------| +| `command` | Имя команды формы → `Form.Command.Имя` | +| `stdCommand` | Стандартная команда → `Form.StandardCommand.Close` и др. | +| `defaultButton: true` | Кнопка по умолчанию | +| `type` | `"usual"`, `"hyperlink"`, `"commandBar"` | +| `picture` | Картинка кнопки | +| `representation` | `"Auto"`, `"Text"`, `"Picture"`, `"PictureAndText"` | +| `locationInCommandBar` | `"Auto"`, `"InCommandBar"`, `"InAdditionalSubmenu"` | + +### Командная панель (cmdBar) + +| Ключ | Описание | +|------|----------| +| `autofill: true` | Автозаполнение стандартными командами | +| `children: [...]` | Кнопки панели | + +### Реквизиты (attributes) + +```json +{ "name": "Объект", "type": "DataProcessorObject.Загрузка", "main": true } +{ "name": "Итого", "type": "decimal(15,2)" } +{ "name": "Таблица", "type": "ValueTable", "columns": [ + { "name": "Номенклатура", "type": "CatalogRef.Номенклатура" }, + { "name": "Количество", "type": "decimal(10,3)" } +]} +``` + +- `savedData: true` — сохраняемые данные + +### Команды (commands) + +```json +{ "name": "Загрузить", "action": "ЗагрузитьОбработка", "shortcut": "Ctrl+Enter" } +``` + +- `title` — заголовок (если отличается от name) +- `picture` — картинка команды + +### Система типов | DSL | XML | |------------------------|----------------------------------------| @@ -79,29 +209,77 @@ powershell.exe -NoProfile -File .claude\skills\form-compile\scripts\form-compile | `"decimal(10,0,nonneg)"` | с AllowedSign=Nonnegative | | `"boolean"` | `xs:boolean` | | `"date"` / `"dateTime"` / `"time"` | `xs:dateTime` + DateFractions | -| `"CatalogRef.Организации"` | `cfg:CatalogRef.Организации` | -| `"DocumentObject.Реализация"` | `cfg:DocumentObject.Реализация` | +| `"CatalogRef.XXX"` | `cfg:CatalogRef.XXX` | +| `"DocumentRef.XXX"` | `cfg:DocumentRef.XXX` | | `"ValueTable"` | `v8:ValueTable` | | `"ValueList"` | `v8:ValueListType` | | `"Type1 \| Type2"` | составной тип | -### Пример JSON +## Связки: элемент + реквизит +Таблица и некоторые поля требуют связанный реквизит. Элемент ссылается на реквизит через `path`. + +**Таблица** — элемент `table` + реквизит `ValueTable`: ```json { - "title": "Загрузка данных", - "properties": { "autoTitle": false }, - "events": { "OnCreateAtServer": "ПриСозданииНаСервере" }, "elements": [ - { "group": "horizontal", "name": "Шапка", "children": [ - { "input": "Организация", "path": "Объект.Организация", "on": ["OnChange"] } - ]}, { "table": "Товары", "path": "Объект.Товары", "columns": [ { "input": "Номенклатура", "path": "Объект.Товары.Номенклатура" } ]} ], "attributes": [ - { "name": "Объект", "type": "DataProcessorObject.ЗагрузкаДанных", "main": true } + { "name": "Объект", "type": "DataProcessorObject.Загрузка", "main": true, + "columns": [ + { "name": "Товары", "type": "ValueTable", "columns": [ + { "name": "Номенклатура", "type": "CatalogRef.Номенклатура" } + ]} + ] + } + ] +} +``` + +Или, если таблица привязана к реквизиту формы (не к Объект): +```json +{ + "elements": [ + { "table": "ТаблицаДанных", "path": "ТаблицаДанных", "columns": [ + { "input": "Наименование", "path": "ТаблицаДанных.Наименование" } + ]} + ], + "attributes": [ + { "name": "ТаблицаДанных", "type": "ValueTable", "columns": [ + { "name": "Наименование", "type": "string(150)" } + ]} + ] +} +``` + +## Паттерны + +### Диалог загрузки файла + +```json +{ + "title": "Загрузка из файла", + "properties": { "autoTitle": false }, + "events": { "OnCreateAtServer": "ПриСозданииНаСервере" }, + "elements": [ + { "group": "horizontal", "name": "ГруппаФайл", "children": [ + { "input": "ИмяФайла", "path": "ИмяФайла", "title": "Файл", "inputHint": "Выберите файл...", "choiceButton": true, "on": ["StartChoice"] }, + { "check": "ПерваяСтрокаЗаголовок", "path": "ПерваяСтрокаЗаголовок" } + ]}, + { "input": "Результат", "path": "Результат", "multiLine": true, "height": 8, "readOnly": true, "title": "Лог" }, + { "group": "horizontal", "name": "ГруппаКнопок", "children": [ + { "button": "Загрузить", "command": "Загрузить", "defaultButton": true }, + { "button": "Закрыть", "stdCommand": "Close" } + ]} + ], + "attributes": [ + { "name": "Объект", "type": "ExternalDataProcessorObject.ЗагрузкаИзФайла", "main": true }, + { "name": "ИмяФайла", "type": "string" }, + { "name": "ПерваяСтрокаЗаголовок", "type": "boolean" }, + { "name": "Результат", "type": "string" } ], "commands": [ { "name": "Загрузить", "action": "ЗагрузитьОбработка", "shortcut": "Ctrl+Enter" } @@ -109,26 +287,84 @@ powershell.exe -NoProfile -File .claude\skills\form-compile\scripts\form-compile } ``` -### Автогенерация +### Мастер (wizard) с шагами -- **Companion-элементы**: ContextMenu, ExtendedTooltip и др. создаются автоматически с правильными именами и ID -- **Обработчики событий**: `"on": ["OnChange"]` → `<Event name="OnChange">ОрганизацияПриИзменении</Event>` -- **Namespace**: все 17 namespace-деклараций добавляются автоматически -- **ID**: последовательная нумерация, AutoCommandBar всегда id="-1" +```json +{ + "title": "Мастер настройки", + "properties": { "autoTitle": false }, + "elements": [ + { "pages": "СтраницыМастера", "pagesRepresentation": "None", "children": [ + { "page": "Шаг1", "title": "Параметры", "children": [ + { "input": "Параметр1", "path": "Параметр1" } + ]}, + { "page": "Шаг2", "title": "Результат", "children": [ + { "input": "Итог", "path": "Итог", "readOnly": true } + ]} + ]}, + { "group": "horizontal", "name": "Навигация", "children": [ + { "button": "Назад", "command": "Назад", "title": "< Назад" }, + { "button": "Далее", "command": "Далее", "title": "Далее >" } + ]} + ], + "attributes": [ + { "name": "Объект", "type": "ExternalDataProcessorObject.Мастер", "main": true }, + { "name": "Параметр1", "type": "string" }, + { "name": "Итог", "type": "string" } + ], + "commands": [ + { "name": "Назад", "action": "НазадОбработка" }, + { "name": "Далее", "action": "ДалееОбработка" } + ] +} +``` + +### Список с фильтром и таблицей + +```json +{ + "title": "Просмотр данных", + "elements": [ + { "group": "horizontal", "name": "Фильтр", "children": [ + { "input": "Период", "path": "Период", "on": ["OnChange"] }, + { "input": "Организация", "path": "Организация", "on": ["OnChange"] } + ]}, + { "table": "Данные", "path": "Данные", "changeRowSet": true, "columns": [ + { "input": "Дата", "path": "Данные.Дата" }, + { "input": "Сумма", "path": "Данные.Сумма" }, + { "input": "Комментарий", "path": "Данные.Комментарий" } + ]} + ], + "attributes": [ + { "name": "Объект", "type": "ExternalDataProcessorObject.Просмотр", "main": true }, + { "name": "Период", "type": "date" }, + { "name": "Организация", "type": "string" }, + { "name": "Данные", "type": "ValueTable", "columns": [ + { "name": "Дата", "type": "date" }, + { "name": "Сумма", "type": "decimal(15,2)" }, + { "name": "Комментарий", "type": "string(200)" } + ]} + ] +} +``` + +## Автогенерация + +- **Companion-элементы**: ContextMenu, ExtendedTooltip и др. создаются автоматически +- **Обработчики событий**: `"on": ["OnChange"]` → `ОрганизацияПриИзменении` +- **Namespace**: все 17 namespace-деклараций +- **ID**: последовательная нумерация, AutoCommandBar = id="-1" +- **Unknown keys**: выводится предупреждение о нераспознанных ключах ## Верификация -Используйте `/form-info` для проверки результата: - ``` -/form-info <OutputPath> +/form-validate <OutputPath> — проверка корректности XML +/form-info <OutputPath> — визуальная сводка структуры ``` -Структура в сводке должна совпадать с определением в JSON. - ## Особенности для внешних обработок (EPF) - **Тип главного реквизита**: `ExternalDataProcessorObject.ИмяОбработки` (не `DataProcessorObject`) - **DataPath**: используйте реквизиты формы (`ИмяРеквизита`), а не `Объект.ИмяРеквизита` — у внешних обработок нет реквизитов объекта в метаданных - **Ссылочные типы**: `CatalogRef.XXX`, `DocumentRef.XXX` и т.д. могут не собраться в пустой базе — используйте `string` или базовые типы для автономной сборки - From 7e0e30730c779683b735c995a301340fa5188913 Mon Sep 17 00:00:00 2001 From: Nick Shirokov <nick_4312@mail.ru> Date: Mon, 9 Feb 2026 16:03:17 +0300 Subject: [PATCH 09/13] Add unknown key warnings to form-add, update form-guide.md form-add: warn about unrecognized DSL keys in element definitions, matching the same validation added to form-compile earlier. form-guide.md: update form-info output examples to reflect new Title-in-header format and DSL-friendly command display (-> Name [cmd], -> Close [std]). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --- .claude/skills/form-add/scripts/form-add.ps1 | 23 ++++++++++++++++++++ docs/form-guide.md | 5 +++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/.claude/skills/form-add/scripts/form-add.ps1 b/.claude/skills/form-add/scripts/form-add.ps1 index 76ad3e28..8604656b 100644 --- a/.claude/skills/form-add/scripts/form-add.ps1 +++ b/.claude/skills/form-add/scripts/form-add.ps1 @@ -560,6 +560,29 @@ function Emit-Element { } if (-not $typeKey) { Write-Warning "Unknown element type, skipping"; return } + # Validate known keys — warn about typos + $knownKeys = @{ + "group"=1;"input"=1;"check"=1;"label"=1;"labelField"=1;"table"=1;"pages"=1;"page"=1 + "button"=1;"picture"=1;"picField"=1;"calendar"=1;"cmdBar"=1;"popup"=1 + "name"=1;"path"=1;"title"=1 + "visible"=1;"hidden"=1;"enabled"=1;"disabled"=1;"readOnly"=1 + "on"=1;"handlers"=1 + "titleLocation"=1;"representation"=1;"width"=1;"height"=1 + "horizontalStretch"=1;"verticalStretch"=1;"autoMaxWidth"=1;"autoMaxHeight"=1 + "multiLine"=1;"passwordMode"=1;"choiceButton"=1;"clearButton"=1 + "spinButton"=1;"dropListButton"=1;"markIncomplete"=1;"skipOnInput"=1;"inputHint"=1 + "hyperlink"=1;"showTitle"=1;"united"=1;"children"=1;"columns"=1 + "changeRowSet"=1;"changeRowOrder"=1;"header"=1;"footer"=1 + "commandBarLocation"=1;"searchStringLocation"=1;"pagesRepresentation"=1 + "type"=1;"command"=1;"stdCommand"=1;"defaultButton"=1;"locationInCommandBar"=1 + "src"=1;"autofill"=1 + } + foreach ($p in $el.PSObject.Properties) { + if (-not $knownKeys.ContainsKey($p.Name)) { + Write-Warning "Element '$($el.$typeKey)': unknown key '$($p.Name)' — ignored." + } + } + $name = Get-ElementName -el $el -typeKey $typeKey $id = New-Id diff --git a/docs/form-guide.md b/docs/form-guide.md index 34f4ed38..d9c8738a 100644 --- a/docs/form-guide.md +++ b/docs/form-guide.md @@ -24,7 +24,7 @@ Claude найдёт Form.xml и вызовет `/form-info`. Результат — компактная сводка (40–100 строк вместо тысяч): ``` -=== Form: ФормаДокумента (Documents.РеализацияТоваровУслуг) === +=== Form: ФормаДокумента — "Реализация товаров и услуг" (Documents.РеализацияТоваровУслуг) === Properties: AutoTitle=false, CommandBarLocation=None @@ -130,7 +130,8 @@ Claude сделает grep по Form.xml и найдёт полный XML-бло ### Привязки и события - `-> Объект.Поле` — DataPath (привязка к данным) -- `-> Form.Command.Имя` — привязка к команде формы +- `-> Имя [cmd]` — привязка к команде формы +- `-> Close [std]` — привязка к стандартной команде - `{OnChange, StartChoice}` — имена подключённых событий - `[title:Текст]` — заголовок, если отличается от имени элемента From c5fa794cf3b65845964f7d1d422e8d69d1351947 Mon Sep 17 00:00:00 2001 From: Nick Shirokov <nick_4312@mail.ru> Date: Mon, 9 Feb 2026 17:39:29 +0300 Subject: [PATCH 10/13] Add event validation and element-level stdCommand support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - stdCommand dot syntax: "Товары.Add" → Form.Item.Товары.StandardCommand.Add - Event name validation: warn on unknown events per element type (13 types, ~60 events) - Form-level event validation (19 known events) - Document events reference and popup element in SKILL.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --- .claude/skills/form-add/SKILL.md | 17 +++++ .claude/skills/form-add/scripts/form-add.ps1 | 59 ++++++++++++++---- .claude/skills/form-compile/SKILL.md | 39 +++++++++++- .../form-compile/scripts/form-compile.ps1 | 62 +++++++++++++++---- 4 files changed, 152 insertions(+), 25 deletions(-) diff --git a/.claude/skills/form-add/SKILL.md b/.claude/skills/form-add/SKILL.md index 0de9be7f..9b93eb05 100644 --- a/.claude/skills/form-add/SKILL.md +++ b/.claude/skills/form-add/SKILL.md @@ -75,6 +75,23 @@ powershell.exe -NoProfile -File .claude\skills\form-add\scripts\form-add.ps1 -Fo Группы и таблицы поддерживают `children`/`columns` для вложенных элементов. +### Кнопки: command и stdCommand + +- `"command": "ИмяКоманды"` → `Form.Command.ИмяКоманды` +- `"stdCommand": "Close"` → `Form.StandardCommand.Close` +- `"stdCommand": "Товары.Add"` → `Form.Item.Товары.StandardCommand.Add` (стандартная команда элемента) + +### Допустимые события (`on`) + +Компилятор предупреждает об ошибках в именах событий. Основные: + +- **input**: `OnChange`, `StartChoice`, `ChoiceProcessing`, `Clearing`, `AutoComplete`, `TextEditEnd` +- **check**: `OnChange` +- **table**: `OnStartEdit`, `OnEditEnd`, `OnChange`, `Selection`, `BeforeAddRow`, `BeforeDeleteRow`, `OnActivateRow` +- **label/picture**: `Click`, `URLProcessing` +- **pages**: `OnCurrentPageChange` +- **button**: `Click` + ### Система типов (для attributes) `string`, `string(100)`, `decimal(15,2)`, `boolean`, `date`, `dateTime`, `CatalogRef.XXX`, `DocumentObject.XXX`, `ValueTable`, `DynamicList`, `Type1 | Type2` (составной). diff --git a/.claude/skills/form-add/scripts/form-add.ps1 b/.claude/skills/form-add/scripts/form-add.ps1 index 8604656b..9f8cc0b6 100644 --- a/.claude/skills/form-add/scripts/form-add.ps1 +++ b/.claude/skills/form-add/scripts/form-add.ps1 @@ -233,9 +233,37 @@ function Get-ElementName { return "$($el.$typeKey)" } +$script:knownEvents = @{ + "input" = @("OnChange","StartChoice","ChoiceProcessing","AutoComplete","TextEditEnd","Clearing","Creating","EditTextChange") + "check" = @("OnChange") + "label" = @("Click","URLProcessing") + "labelField"= @("OnChange","StartChoice","ChoiceProcessing","Click","URLProcessing","Clearing") + "table" = @("Selection","BeforeAddRow","AfterDeleteRow","BeforeDeleteRow","OnActivateRow","OnEditEnd","OnStartEdit","BeforeRowChange","BeforeEditEnd","ValueChoice","OnActivateCell","OnActivateField","Drag","DragStart","DragCheck","DragEnd","OnGetDataAtServer","BeforeLoadUserSettingsAtServer","OnUpdateUserSettingSetAtServer","OnChange") + "pages" = @("OnCurrentPageChange") + "page" = @("OnCurrentPageChange") + "button" = @("Click") + "picField" = @("OnChange","StartChoice","ChoiceProcessing","Click","Clearing") + "calendar" = @("OnChange","OnActivate") + "picture" = @("Click") + "cmdBar" = @() + "popup" = @() + "group" = @() +} + function Emit-Events { - param($el, [string]$elementName, [string]$indent) + param($el, [string]$elementName, [string]$indent, [string]$typeKey) if (-not $el.on) { return } + + # Validate event names + if ($typeKey -and $script:knownEvents.ContainsKey($typeKey)) { + $allowed = $script:knownEvents[$typeKey] + foreach ($evt in $el.on) { + if ($allowed.Count -gt 0 -and $allowed -notcontains "$evt") { + Write-Host "[WARN] Unknown event '$evt' for $typeKey '$elementName'. Known: $($allowed -join ', ')" + } + } + } + X "$indent<Events>" foreach ($evt in $el.on) { $evtName = "$evt" @@ -323,7 +351,7 @@ function Emit-Input { if ($el.inputHint) { Emit-MLText -tag "InputHint" -text "$($el.inputHint)" -indent $inner } Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner - Emit-Events -el $el -elementName $name -indent $inner + Emit-Events -el $el -elementName $name -indent $inner -typeKey "input" X "$indent</InputField>" } @@ -337,7 +365,7 @@ function Emit-Check { if ($el.titleLocation) { X "$inner<TitleLocation>$($el.titleLocation)</TitleLocation>" } Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner - Emit-Events -el $el -elementName $name -indent $inner + Emit-Events -el $el -elementName $name -indent $inner -typeKey "check" X "$indent</CheckBoxField>" } @@ -362,7 +390,7 @@ function Emit-Label { if ($el.height) { X "$inner<Height>$($el.height)</Height>" } Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner - Emit-Events -el $el -elementName $name -indent $inner + Emit-Events -el $el -elementName $name -indent $inner -typeKey "label" X "$indent</LabelDecoration>" } @@ -376,7 +404,7 @@ function Emit-LabelField { if ($el.hyperlink -eq $true) { X "$inner<Hyperlink>true</Hyperlink>" } Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner - Emit-Events -el $el -elementName $name -indent $inner + Emit-Events -el $el -elementName $name -indent $inner -typeKey "labelField" X "$indent</LabelField>" } @@ -405,7 +433,7 @@ function Emit-Table { foreach ($col in $el.columns) { Emit-Element -el $col -indent "$inner`t" } X "$inner</ChildItems>" } - Emit-Events -el $el -elementName $name -indent $inner + Emit-Events -el $el -elementName $name -indent $inner -typeKey "table" X "$indent</Table>" } @@ -416,7 +444,7 @@ function Emit-Pages { if ($el.pagesRepresentation) { X "$inner<PagesRepresentation>$($el.pagesRepresentation)</PagesRepresentation>" } Emit-CommonFlags -el $el -indent $inner Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner - Emit-Events -el $el -elementName $name -indent $inner + Emit-Events -el $el -elementName $name -indent $inner -typeKey "pages" if ($el.children -and $el.children.Count -gt 0) { X "$inner<ChildItems>" foreach ($child in $el.children) { Emit-Element -el $child -indent "$inner`t" } @@ -453,7 +481,14 @@ function Emit-Button { X "$inner<Type>$btnType</Type>" } if ($el.command) { X "$inner<CommandName>Form.Command.$($el.command)</CommandName>" } - if ($el.stdCommand) { X "$inner<CommandName>Form.StandardCommand.$($el.stdCommand)</CommandName>" } + if ($el.stdCommand) { + $sc = "$($el.stdCommand)" + if ($sc -match '^(.+)\.(.+)$') { + X "$inner<CommandName>Form.Item.$($Matches[1]).StandardCommand.$($Matches[2])</CommandName>" + } else { + X "$inner<CommandName>Form.StandardCommand.$sc</CommandName>" + } + } Emit-Title -el $el -name $name -indent $inner Emit-CommonFlags -el $el -indent $inner if ($el.defaultButton -eq $true) { X "$inner<DefaultButton>true</DefaultButton>" } @@ -466,7 +501,7 @@ function Emit-Button { if ($el.representation) { X "$inner<Representation>$($el.representation)</Representation>" } if ($el.locationInCommandBar) { X "$inner<LocationInCommandBar>$($el.locationInCommandBar)</LocationInCommandBar>" } Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner - Emit-Events -el $el -elementName $name -indent $inner + Emit-Events -el $el -elementName $name -indent $inner -typeKey "button" X "$indent</Button>" } @@ -485,7 +520,7 @@ function Emit-PictureDecoration { if ($el.height) { X "$inner<Height>$($el.height)</Height>" } Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner - Emit-Events -el $el -elementName $name -indent $inner + Emit-Events -el $el -elementName $name -indent $inner -typeKey "picture" X "$indent</PictureDecoration>" } @@ -500,7 +535,7 @@ function Emit-PictureField { if ($el.height) { X "$inner<Height>$($el.height)</Height>" } Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner - Emit-Events -el $el -elementName $name -indent $inner + Emit-Events -el $el -elementName $name -indent $inner -typeKey "picField" X "$indent</PictureField>" } @@ -513,7 +548,7 @@ function Emit-Calendar { Emit-CommonFlags -el $el -indent $inner Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner - Emit-Events -el $el -elementName $name -indent $inner + Emit-Events -el $el -elementName $name -indent $inner -typeKey "calendar" X "$indent</CalendarField>" } diff --git a/.claude/skills/form-compile/SKILL.md b/.claude/skills/form-compile/SKILL.md index 3e8a6138..7720bffa 100644 --- a/.claude/skills/form-compile/SKILL.md +++ b/.claude/skills/form-compile/SKILL.md @@ -85,6 +85,26 @@ powershell.exe -NoProfile -File .claude\skills\form-compile\scripts\form-compile | `on: [...]` | События с автоименованием обработчиков | | `handlers: {...}` | Явное задание имён обработчиков: `{"OnChange": "МоёИмя"}` | +### Допустимые имена событий (`on`) + +Компилятор предупреждает о неизвестных событиях. Имена регистрозависимы — используйте точно как указано. + +**Форма** (`events`): `OnCreateAtServer`, `OnOpen`, `BeforeClose`, `OnClose`, `NotificationProcessing`, `ChoiceProcessing`, `OnReadAtServer`, `BeforeWriteAtServer`, `OnWriteAtServer`, `AfterWriteAtServer`, `BeforeWrite`, `AfterWrite`, `FillCheckProcessingAtServer`, `BeforeLoadDataFromSettingsAtServer`, `OnLoadDataFromSettingsAtServer`, `ExternalEvent`, `Opening` + +**input / picField**: `OnChange`, `StartChoice`, `ChoiceProcessing`, `AutoComplete`, `TextEditEnd`, `Clearing`, `Creating`, `EditTextChange` + +**check**: `OnChange` + +**table**: `OnStartEdit`, `OnEditEnd`, `OnChange`, `Selection`, `ValueChoice`, `BeforeAddRow`, `BeforeDeleteRow`, `AfterDeleteRow`, `BeforeRowChange`, `BeforeEditEnd`, `OnActivateRow`, `OnActivateCell`, `Drag`, `DragStart`, `DragCheck`, `DragEnd` + +**label / picture**: `Click`, `URLProcessing` + +**labelField**: `OnChange`, `StartChoice`, `ChoiceProcessing`, `Click`, `URLProcessing`, `Clearing` + +**button**: `Click` + +**pages**: `OnCurrentPageChange` + ### Поле ввода (input) | Ключ | Описание | Пример | @@ -164,7 +184,7 @@ powershell.exe -NoProfile -File .claude\skills\form-compile\scripts\form-compile | Ключ | Описание | |------|----------| | `command` | Имя команды формы → `Form.Command.Имя` | -| `stdCommand` | Стандартная команда → `Form.StandardCommand.Close` и др. | +| `stdCommand` | Стандартная команда: `"Close"` → `Form.StandardCommand.Close`; с точкой: `"Товары.Add"` → `Form.Item.Товары.StandardCommand.Add` | | `defaultButton: true` | Кнопка по умолчанию | | `type` | `"usual"`, `"hyperlink"`, `"commandBar"` | | `picture` | Картинка кнопки | @@ -178,6 +198,23 @@ powershell.exe -NoProfile -File .claude\skills\form-compile\scripts\form-compile | `autofill: true` | Автозаполнение стандартными командами | | `children: [...]` | Кнопки панели | +### Выпадающее меню (popup) + +| Ключ | Описание | +|------|----------| +| `title` | Заголовок подменю | +| `children: [...]` | Кнопки подменю | + +Используется внутри `cmdBar` для группировки кнопок в подменю: +```json +{ "cmdBar": "Панель", "children": [ + { "popup": "Добавить", "title": "Добавить", "children": [ + { "button": "ДобавитьСтроку", "stdCommand": "Товары.Add" }, + { "button": "ДобавитьИзДокумента", "command": "ДобавитьИзДокумента", "title": "Из документа" } + ]} +]} +``` + ### Реквизиты (attributes) ```json diff --git a/.claude/skills/form-compile/scripts/form-compile.ps1 b/.claude/skills/form-compile/scripts/form-compile.ps1 index 3d0adc66..496551e0 100644 --- a/.claude/skills/form-compile/scripts/form-compile.ps1 +++ b/.claude/skills/form-compile/scripts/form-compile.ps1 @@ -229,11 +229,39 @@ function Get-ElementName { return "$($el.$typeKey)" } +$script:knownEvents = @{ + "input" = @("OnChange","StartChoice","ChoiceProcessing","AutoComplete","TextEditEnd","Clearing","Creating","EditTextChange") + "check" = @("OnChange") + "label" = @("Click","URLProcessing") + "labelField"= @("OnChange","StartChoice","ChoiceProcessing","Click","URLProcessing","Clearing") + "table" = @("Selection","BeforeAddRow","AfterDeleteRow","BeforeDeleteRow","OnActivateRow","OnEditEnd","OnStartEdit","BeforeRowChange","BeforeEditEnd","ValueChoice","OnActivateCell","OnActivateField","Drag","DragStart","DragCheck","DragEnd","OnGetDataAtServer","BeforeLoadUserSettingsAtServer","OnUpdateUserSettingSetAtServer","OnChange") + "pages" = @("OnCurrentPageChange") + "page" = @("OnCurrentPageChange") + "button" = @("Click") + "picField" = @("OnChange","StartChoice","ChoiceProcessing","Click","Clearing") + "calendar" = @("OnChange","OnActivate") + "picture" = @("Click") + "cmdBar" = @() + "popup" = @() + "group" = @() +} +$script:knownFormEvents = @("OnCreateAtServer","OnOpen","BeforeClose","OnClose","NotificationProcessing","ChoiceProcessing","OnReadAtServer","AfterWriteAtServer","BeforeWriteAtServer","AfterWrite","BeforeWrite","OnWriteAtServer","FillCheckProcessingAtServer","OnLoadDataFromSettingsAtServer","BeforeLoadDataFromSettingsAtServer","OnSaveDataInSettingsAtServer","ExternalEvent","OnReopen","Opening") + function Emit-Events { - param($el, [string]$elementName, [string]$indent) + param($el, [string]$elementName, [string]$indent, [string]$typeKey) if (-not $el.on) { return } + # Validate event names + if ($typeKey -and $script:knownEvents.ContainsKey($typeKey)) { + $allowed = $script:knownEvents[$typeKey] + foreach ($evt in $el.on) { + if ($allowed.Count -gt 0 -and $allowed -notcontains "$evt") { + Write-Host "[WARN] Unknown event '$evt' for $typeKey '$elementName'. Known: $($allowed -join ', ')" + } + } + } + X "$indent<Events>" foreach ($evt in $el.on) { $evtName = "$evt" @@ -454,7 +482,7 @@ function Emit-Input { Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner - Emit-Events -el $el -elementName $name -indent $inner + Emit-Events -el $el -elementName $name -indent $inner -typeKey "input" X "$indent</InputField>" } @@ -478,7 +506,7 @@ function Emit-Check { Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner - Emit-Events -el $el -elementName $name -indent $inner + Emit-Events -el $el -elementName $name -indent $inner -typeKey "check" X "$indent</CheckBoxField>" } @@ -511,7 +539,7 @@ function Emit-Label { Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner - Emit-Events -el $el -elementName $name -indent $inner + Emit-Events -el $el -elementName $name -indent $inner -typeKey "label" X "$indent</LabelDecoration>" } @@ -533,7 +561,7 @@ function Emit-LabelField { Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner - Emit-Events -el $el -elementName $name -indent $inner + Emit-Events -el $el -elementName $name -indent $inner -typeKey "labelField" X "$indent</LabelField>" } @@ -581,7 +609,7 @@ function Emit-Table { X "$inner</ChildItems>" } - Emit-Events -el $el -elementName $name -indent $inner + Emit-Events -el $el -elementName $name -indent $inner -typeKey "table" X "$indent</Table>" } @@ -601,7 +629,7 @@ function Emit-Pages { # Companion Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner - Emit-Events -el $el -elementName $name -indent $inner + Emit-Events -el $el -elementName $name -indent $inner -typeKey "pages" # Children (pages) if ($el.children -and $el.children.Count -gt 0) { @@ -672,7 +700,12 @@ function Emit-Button { X "$inner<CommandName>Form.Command.$($el.command)</CommandName>" } if ($el.stdCommand) { - X "$inner<CommandName>Form.StandardCommand.$($el.stdCommand)</CommandName>" + $sc = "$($el.stdCommand)" + if ($sc -match '^(.+)\.(.+)$') { + X "$inner<CommandName>Form.Item.$($Matches[1]).StandardCommand.$($Matches[2])</CommandName>" + } else { + X "$inner<CommandName>Form.StandardCommand.$sc</CommandName>" + } } Emit-Title -el $el -name $name -indent $inner @@ -699,7 +732,7 @@ function Emit-Button { # Companion Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner - Emit-Events -el $el -elementName $name -indent $inner + Emit-Events -el $el -elementName $name -indent $inner -typeKey "button" X "$indent</Button>" } @@ -729,7 +762,7 @@ function Emit-PictureDecoration { Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner - Emit-Events -el $el -elementName $name -indent $inner + Emit-Events -el $el -elementName $name -indent $inner -typeKey "picture" X "$indent</PictureDecoration>" } @@ -752,7 +785,7 @@ function Emit-PictureField { Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner - Emit-Events -el $el -elementName $name -indent $inner + Emit-Events -el $el -elementName $name -indent $inner -typeKey "picField" X "$indent</PictureField>" } @@ -772,7 +805,7 @@ function Emit-Calendar { Emit-Companion -tag "ContextMenu" -name "${name}КонтекстноеМеню" -indent $inner Emit-Companion -tag "ExtendedTooltip" -name "${name}РасширеннаяПодсказка" -indent $inner - Emit-Events -el $el -elementName $name -indent $inner + Emit-Events -el $el -elementName $name -indent $inner -typeKey "calendar" X "$indent</CalendarField>" } @@ -1058,6 +1091,11 @@ X "`t</AutoCommandBar>" # 12e. Events if ($def.events) { + foreach ($p in $def.events.PSObject.Properties) { + if ($script:knownFormEvents -notcontains $p.Name) { + Write-Host "[WARN] Unknown form event '$($p.Name)'. Known: $($script:knownFormEvents -join ', ')" + } + } X "`t<Events>" foreach ($p in $def.events.PSObject.Properties) { X "`t`t<Event name=`"$($p.Name)`">$($p.Value)</Event>" From 5c07dec82ce376027f3868e63d4d2c473cf95516 Mon Sep 17 00:00:00 2001 From: Nick Shirokov <nick_4312@mail.ru> Date: Mon, 9 Feb 2026 18:12:17 +0300 Subject: [PATCH 11/13] Add form layout patterns guide with real-world conventions Design patterns extracted from 1C:Accounting 8.3.24 forms: - 5 archetypes: document, processor, list, catalog, wizard - Naming conventions for groups, elements, event handlers - Layout principles (2-column header, footer totals, filter pairs) - Two complete DSL examples (processing form, list with filters) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --- .claude/skills/form-compile/SKILL.md | 2 + docs/form-guide.md | 1 + docs/form-patterns.md | 219 +++++++++++++++++++++++++++ 3 files changed, 222 insertions(+) create mode 100644 docs/form-patterns.md diff --git a/.claude/skills/form-compile/SKILL.md b/.claude/skills/form-compile/SKILL.md index 7720bffa..d8ecfa9b 100644 --- a/.claude/skills/form-compile/SKILL.md +++ b/.claude/skills/form-compile/SKILL.md @@ -13,6 +13,8 @@ allowed-tools: Принимает компактное JSON-определение формы (20–50 строк) и генерирует полный корректный Form.xml (100–500+ строк) с namespace-декларациями, автогенерированными companion-элементами, последовательными ID. +> При проектировании формы с нуля — см. [паттерны компоновки](../../docs/form-patterns.md): типовые архетипы (документ, обработка, список, справочник, мастер), конвенции именования, примеры DSL. + ## Использование ``` diff --git a/docs/form-guide.md b/docs/form-guide.md index d9c8738a..e408cb75 100644 --- a/docs/form-guide.md +++ b/docs/form-guide.md @@ -211,3 +211,4 @@ Claude создаст JSON-определение и вызовет `/form-compi - [Управляемая форма](1c-form-spec.md) — Form.xml, элементы, команды, реквизиты, система типов - [Form DSL](form-dsl-spec.md) — JSON-формат описания формы для `/form-compile` и `/form-add` +- [Паттерны компоновки](form-patterns.md) — типовые архетипы форм, конвенции именования, примеры DSL diff --git a/docs/form-patterns.md b/docs/form-patterns.md new file mode 100644 index 00000000..5e9ef562 --- /dev/null +++ b/docs/form-patterns.md @@ -0,0 +1,219 @@ +# Паттерны компоновки управляемых форм + +Рекомендации по дизайну форм, извлечённые из типовых конфигураций 1С. Используйте при создании форм через `/form-compile`, когда требования пользователя не детализируют расположение элементов. + +## Архетипы форм + +### Форма документа + +``` +Шапка (horizontal, 2 колонки) +├─ Левая (vertical): НомерДата (H: Номер + Дата "от"), Контрагент, Договор +├─ Правая (vertical): Организация, Подразделение, ЦеныИВалюта (надпись-ссылка) +Страницы (pages) +├─ Товары: таблица Объект.Товары +├─ Услуги: таблица Объект.Услуги (опционально) +└─ Дополнительно: прочие реквизиты +Подвал (vertical) +├─ Итоги (horizontal): Всего, НДС, Скидка +└─ КомментарийОтветственный (horizontal): Комментарий + Ответственный +``` + +**Типичные события:** OnCreateAtServer, OnReadAtServer, OnOpen, BeforeWriteAtServer, AfterWriteAtServer, AfterWrite, NotificationProcessing + +**Свойства:** autoTitle=false, командная панель со стандартными + глобальными командами + +### Форма обработки (DataProcessor) + +``` +Параметры (vertical) +├─ Группа полей ввода (Организация, Период, режимы работы) +├─ Информационные надписи (label, hyperlink) +Рабочая область +├─ Таблица данных или Pages с вкладками +Кнопки действий +├─ Выполнить / Применить (defaultButton) +├─ Закрыть (stdCommand: Close) +``` + +**Типичные события:** OnCreateAtServer, OnOpen, NotificationProcessing + +**Свойства:** windowOpeningMode=LockOwnerWindow (если диалог), autoTitle=false + +### Форма списка + +``` +Отборы (group: alwaysHorizontal) +├─ ГруппаОтбор[Поле] (H): Флажок + Поле ввода (для каждого фильтра) +Список (table, DynamicList) +├─ Колонки: labelField (не input — данные только для чтения) +``` + +**Типичные события:** OnCreateAtServer, OnOpen, NotificationProcessing, OnLoadDataFromSettingsAtServer + +**Свойства:** autoSaveDataInSettings=Use (запомнить отборы) + +**Фильтры:** пара реквизитов на каждый фильтр — `Отбор[Поле]` (значение) + `Отбор[Поле]Использование` (boolean, флажок вкл/выкл) + +### Форма элемента справочника + +**Простая:** +``` +ГруппаРеквизитов (horizontal) +├─ Наименование -> Объект.Description +└─ Код -> Объект.Code (если нужен) +``` + +**Сложная:** +``` +Главное (vertical) +├─ Наименование -> Объект.Description +├─ Параметры (horizontal, 2 колонки) +│ ├─ Левая: основные реквизиты +│ └─ Правая: дополнительные реквизиты +└─ КонтактныеДанные / Дополнительно (vertical) +``` + +**Типичные события:** OnCreateAtServer, OnReadAtServer, BeforeWriteAtServer, NotificationProcessing + +### Мастер (Wizard) + +``` +Страницы (pages, OnCurrentPageChange) +├─ Шаг1: описание + параметры +├─ Шаг2: основная работа +└─ Шаг3: результат +Кнопки (horizontal) +├─ Назад (command), Далее (command, defaultButton), Выполнить (command) +└─ Закрыть (stdCommand: Close) +``` + +**Свойства:** windowOpeningMode=LockOwnerWindow + +## Конвенции именования + +### Группы + +| Назначение | Имя | Тип | +|-----------|-----|-----| +| Шапка | `ГруппаШапка` | horizontal | +| Левая колонка | `ГруппаШапкаЛевая` | vertical | +| Правая колонка | `ГруппаШапкаПравая` | vertical | +| Номер+Дата | `ГруппаНомерДата` | horizontal | +| Подвал | `ГруппаПодвал` | vertical | +| Итоги | `ГруппаИтоги` | horizontal | +| Кнопки | `ГруппаКнопок` | horizontal | +| Страницы | `ГруппаСтраницы` / `Страницы` | pages | + +### Элементы + +| Назначение | Имя | Суффикс | +|-----------|-----|---------| +| Поле в таблице | `[Таблица][Поле]` | — | +| Итог | `Итоги[Поле]` | — | +| Надпись-ссылка | `[Поле]Надпись` | — | +| Фильтр | `Отбор[Поле]` | — | +| Флажок фильтра | `Отбор[Поле]Использование` | — | +| Кнопка команды | `[Команда]Кнопка` | — | + +### Обработчики событий + +Имя обработчика = имя элемента + суффикс события на русском: + +| Событие | Суффикс | Пример | +|---------|---------|--------| +| OnChange | ПриИзменении | `ОрганизацияПриИзменении` | +| StartChoice | НачалоВыбора | `КонтрагентНачалоВыбора` | +| Click | Нажатие | `ЦеныИВалютаНажатие` | +| OnEditEnd | ПриОкончанииРедактирования | `ТоварыПриОкончанииРедактирования` | +| OnStartEdit | ПриНачалеРедактирования | `ТоварыПриНачалеРедактирования` | + +Обработчики формы — стандартные имена: `ПриСозданииНаСервере`, `ПриОткрытии`, `ПередЗакрытием`, `ОбработкаОповещения`. + +## Принципы компоновки + +1. **Порядок чтения.** Сверху вниз, слева направо. Самое важное — вверху. +2. **Двухколоночная шапка.** Основные реквизиты слева (контрагент, склад), организационные справа (организация, подразделение). +3. **Кнопки действий внизу.** Главная кнопка — `defaultButton: true`. Закрыть — всегда последняя. +4. **Таблицы — основная область.** Табличные части занимают большую часть формы, обычно на Pages. +5. **Итоги рядом с таблицей.** В подвале, горизонтальная группа, все поля readOnly. +6. **Фильтры — отдельная зона.** Над списком, горизонтальная группа (alwaysHorizontal), пара "флажок + поле" на каждый фильтр. +7. **Скрытые элементы для состояний.** Баннеры, предупреждения — `visible: false` по умолчанию, показываются программно. +8. **Надписи-ссылки для диалогов.** `labelField` с `hyperlink: true` и событием Click — для открытия подформ (ЦеныИВалюта, УчётнаяПолитика). + +## Примеры DSL + +### Типичная форма обработки + +```json +{ + "title": "Загрузка данных из CSV", + "properties": { "autoTitle": false, "windowOpeningMode": "LockOwnerWindow" }, + "events": { "OnCreateAtServer": "ПриСозданииНаСервере" }, + "elements": [ + { "group": "vertical", "name": "ГруппаПараметры", "children": [ + { "input": "ФайлЗагрузки", "path": "ФайлЗагрузки", "title": "Файл", "clearButton": true, "horizontalStretch": true, "on": ["StartChoice"] }, + { "input": "Кодировка", "path": "Кодировка", "title": "Кодировка" }, + { "input": "Разделитель", "path": "Разделитель", "title": "Разделитель колонок" } + ]}, + { "table": "Данные", "path": "Объект.Данные", "on": ["OnStartEdit"], "columns": [ + { "input": "ДанныеНомерСтроки", "path": "Объект.Данные.LineNumber", "readOnly": true, "title": "№" }, + { "input": "ДанныеНаименование", "path": "Объект.Данные.Наименование" }, + { "input": "ДанныеКоличество", "path": "Объект.Данные.Количество", "on": ["OnChange"] }, + { "input": "ДанныеСумма", "path": "Объект.Данные.Сумма", "readOnly": true } + ]}, + { "group": "horizontal", "name": "ГруппаКнопок", "children": [ + { "button": "Загрузить", "command": "Загрузить", "title": "Загрузить из файла", "defaultButton": true }, + { "button": "Очистить", "command": "Очистить", "title": "Очистить таблицу" }, + { "button": "Закрыть", "stdCommand": "Close" } + ]} + ], + "attributes": [ + { "name": "Объект", "type": "ExternalDataProcessorObject.ЗагрузкаИзCSV", "main": true }, + { "name": "ФайлЗагрузки", "type": "string" }, + { "name": "Кодировка", "type": "string(20)" }, + { "name": "Разделитель", "type": "string(5)" } + ], + "commands": [ + { "name": "Загрузить", "action": "ЗагрузитьОбработка" }, + { "name": "Очистить", "action": "ОчиститьОбработка" } + ] +} +``` + +### Типичная форма со списком и фильтрами + +```json +{ + "properties": { "autoTitle": false, "autoSaveDataInSettings": "Use" }, + "events": { + "OnCreateAtServer": "ПриСозданииНаСервере", + "NotificationProcessing": "ОбработкаОповещения" + }, + "elements": [ + { "group": "alwaysHorizontal", "name": "ГруппаОтборы", "children": [ + { "group": "horizontal", "name": "ГруппаОтборОрганизация", "children": [ + { "check": "ОтборОрганизацияИспользование", "path": "ОтборОрганизацияИспользование", "titleLocation": "none", "on": ["OnChange"] }, + { "input": "ОтборОрганизация", "path": "ОтборОрганизация", "title": "Организация", "on": ["OnChange"] } + ]}, + { "group": "horizontal", "name": "ГруппаОтборКонтрагент", "children": [ + { "check": "ОтборКонтрагентИспользование", "path": "ОтборКонтрагентИспользование", "titleLocation": "none", "on": ["OnChange"] }, + { "input": "ОтборКонтрагент", "path": "ОтборКонтрагент", "title": "Контрагент", "on": ["OnChange"] } + ]} + ]}, + { "table": "Список", "path": "Список", "on": ["Selection", "OnActivateRow"], "columns": [ + { "labelField": "СписокДата", "path": "Список.Дата", "title": "Дата" }, + { "labelField": "СписокНомер", "path": "Список.Номер", "title": "Номер" }, + { "labelField": "СписокКонтрагент", "path": "Список.Контрагент" }, + { "labelField": "СписокСумма", "path": "Список.Сумма" } + ]} + ], + "attributes": [ + { "name": "Список", "type": "DynamicList", "mainTable": "Document.РеализацияТоваров" }, + { "name": "ОтборОрганизация", "type": "CatalogRef.Организации" }, + { "name": "ОтборОрганизацияИспользование", "type": "boolean" }, + { "name": "ОтборКонтрагент", "type": "CatalogRef.Контрагенты" }, + { "name": "ОтборКонтрагентИспользование", "type": "boolean" } + ] +} +``` From dc9e1fe3dbf17e39ac59c83c40acd4f73fdd088e Mon Sep 17 00:00:00 2001 From: Nick Shirokov <nick_4312@mail.ru> Date: Mon, 9 Feb 2026 18:35:02 +0300 Subject: [PATCH 12/13] Add advanced ERP patterns to form layout guide MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Collapsible groups, status banners, popup menus, custom command bars, hyperlink labels — with DSL examples for each pattern. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --- docs/form-patterns.md | 118 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) diff --git a/docs/form-patterns.md b/docs/form-patterns.md index 5e9ef562..870eab36 100644 --- a/docs/form-patterns.md +++ b/docs/form-patterns.md @@ -104,6 +104,8 @@ | Итоги | `ГруппаИтоги` | horizontal | | Кнопки | `ГруппаКнопок` | horizontal | | Страницы | `ГруппаСтраницы` / `Страницы` | pages | +| Предупреждение | `ГруппаПредупреждение` | horizontal, visible:false | +| Доп. секция | `ГруппаДополнительно` / `ГруппаПрочее` | vertical, collapse | ### Элементы @@ -115,6 +117,9 @@ | Фильтр | `Отбор[Поле]` | — | | Флажок фильтра | `Отбор[Поле]Использование` | — | | Кнопка команды | `[Команда]Кнопка` | — | +| Баннер-картинка | `[Баннер]Картинка` | — | +| Баннер-надпись | `[Баннер]Надпись` | — | +| Подменю | `Подменю[Действие]` | — | ### Обработчики событий @@ -141,6 +146,119 @@ 7. **Скрытые элементы для состояний.** Баннеры, предупреждения — `visible: false` по умолчанию, показываются программно. 8. **Надписи-ссылки для диалогов.** `labelField` с `hyperlink: true` и событием Click — для открытия подформ (ЦеныИВалюта, УчётнаяПолитика). +## Продвинутые паттерны (ERP) + +Извлечены из конфигурации «Управление предприятием» (ERP 8.3.24). Применяйте в сложных формах. + +### Сворачиваемые группы (Collapsible) + +Для необязательных секций — «Подписи», «Дополнительно», «Прочее». Сворачиваются по умолчанию, экономят место. + +``` +ГруппаПодписи (vertical, collapse, collapsed) +├─ Руководитель -> Объект.Руководитель +└─ ГлавныйБухгалтер -> Объект.ГлавныйБухгалтер +``` + +DSL: +```json +{ "group": "vertical", "name": "ГруппаПодписи", "title": "Подписи", + "behavior": "Collapsible", "collapsed": true, "children": [ + { "input": "Руководитель", "path": "Объект.Руководитель" }, + { "input": "ГлавныйБухгалтер", "path": "Объект.ГлавныйБухгалтер" } +]} +``` + +### Баннер-предупреждение (Status Banner) + +Группа «картинка + надпись» без заголовка, скрыта по умолчанию. Показывается программно при определённых условиях (просрочка, блокировка, информация). + +``` +ГруппаПредупреждение (horizontal, showTitle:false, visible:false) +├─ [Picture] ПредупреждениеКартинка -> StdPicture.Information +└─ [Label] ПредупреждениеНадпись (maxWidth:76, textColor:style:ПоясняющийТекст) +``` + +DSL: +```json +{ "group": "horizontal", "name": "ГруппаПредупреждение", "showTitle": false, + "visible": false, "children": [ + { "picture": "ПредупреждениеКартинка" }, + { "label": "ПредупреждениеНадпись", "title": "Текст предупреждения", + "maxWidth": 76, "autoMaxWidth": false } +]} +``` + +### Выпадающее меню в командной панели (Popup) + +Группировка связанных команд (печать, отправка, выгрузка) в одну кнопку-меню с иконкой. + +``` +[CmdBar] КоманднаяПанель +├─ [Popup] ПодменюПечать (picture: StdPicture.Print, representation: Picture) +│ ├─ [Button] ПечатьНакладная -> Печать [cmd] +│ └─ [Button] ПечатьСчёт -> ПечатьСчёт [cmd] +└─ [Popup] ПодменюОтправить (picture: StdPicture.SendByEmail) + └─ [Button] ОтправитьПоПочте -> Отправить [cmd] +``` + +DSL: +```json +{ "cmdBar": "КоманднаяПанель", "children": [ + { "popup": "ПодменюПечать", "title": "Печать", + "picture": "StdPicture.Print", "representation": "Picture", "children": [ + { "button": "ПечатьНакладная", "command": "Печать" }, + { "button": "ПечатьСчёт", "command": "ПечатьСчёт" } + ]}, + { "popup": "ПодменюОтправить", "title": "Отправить", + "picture": "StdPicture.SendByEmail", "representation": "Picture", "children": [ + { "button": "ОтправитьПоПочте", "command": "Отправить" } + ]} +]} +``` + +### Форма без стандартной командной панели + +Для модальных диалогов и мастеров — отключение стандартной командной панели, полностью ручное управление кнопками. + +``` +properties: commandBarLocation=None, windowOpeningMode=LockWholeInterface +Содержимое (vertical) +├─ ... рабочая область ... +ГруппаКнопок (horizontal) +├─ Назад (command), Далее (command, defaultButton) +└─ Закрыть (stdCommand: Close) +``` + +DSL: +```json +{ + "properties": { "commandBarLocation": "None", "windowOpeningMode": "LockWholeInterface" }, + "elements": [ + { "group": "vertical", "name": "Содержимое", "children": [ "..." ] }, + { "group": "horizontal", "name": "ГруппаКнопок", "children": [ + { "button": "Назад", "command": "Назад" }, + { "button": "Далее", "command": "Далее", "defaultButton": true }, + { "button": "Закрыть", "stdCommand": "Close" } + ]} + ] +} +``` + +### Надпись-гиперссылка для открытия подформ + +`labelField` с `hyperlink: true` и событием Click — вместо кнопки. Типичный приём для «ЦеныИВалюта», «УчётнаяПолитика» и подобных. + +``` +[LabelField] ЦеныИВалютаНадпись -> ЦеныИВалюта (hyperlink) {Click} +``` + +DSL: +```json +{ "labelField": "ЦеныИВалютаНадпись", "path": "ЦеныИВалюта", + "hyperlink": true, "on": ["Click"] } +``` + ## Примеры DSL ### Типичная форма обработки From ecfb473827d55f58b75f83b4a2594c4f142add9f Mon Sep 17 00:00:00 2001 From: Nick Shirokov <nick_4312@mail.ru> Date: Mon, 9 Feb 2026 18:50:52 +0300 Subject: [PATCH 13/13] Add /form-patterns skill for loading layout guide into context Self-contained SKILL.md with all patterns inlined (no external file dependencies). Archetypes, naming conventions, ERP patterns, DSL examples. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --- .claude/skills/form-compile/SKILL.md | 2 +- .claude/skills/form-patterns/SKILL.md | 253 ++++++++++++++++++++++++++ docs/form-guide.md | 1 + 3 files changed, 255 insertions(+), 1 deletion(-) create mode 100644 .claude/skills/form-patterns/SKILL.md diff --git a/.claude/skills/form-compile/SKILL.md b/.claude/skills/form-compile/SKILL.md index d8ecfa9b..f443a40d 100644 --- a/.claude/skills/form-compile/SKILL.md +++ b/.claude/skills/form-compile/SKILL.md @@ -13,7 +13,7 @@ allowed-tools: Принимает компактное JSON-определение формы (20–50 строк) и генерирует полный корректный Form.xml (100–500+ строк) с namespace-декларациями, автогенерированными companion-элементами, последовательными ID. -> При проектировании формы с нуля — см. [паттерны компоновки](../../docs/form-patterns.md): типовые архетипы (документ, обработка, список, справочник, мастер), конвенции именования, примеры DSL. +> **При проектировании формы с нуля (5+ элементов или нечёткие требования)** — вызовите `/form-patterns` для загрузки справочника: архетипы, конвенции именования, продвинутые паттерны. Для простых форм (1–3 поля, пользователь описал что нужно) — не нужно. ## Использование diff --git a/.claude/skills/form-patterns/SKILL.md b/.claude/skills/form-patterns/SKILL.md new file mode 100644 index 00000000..41583e72 --- /dev/null +++ b/.claude/skills/form-patterns/SKILL.md @@ -0,0 +1,253 @@ +--- +name: form-patterns +description: Справочник паттернов компоновки управляемых форм 1С — архетипы, конвенции именования, продвинутые приёмы +argument-hint: (no arguments) +allowed-tools: [] +--- + +# /form-patterns — паттерны компоновки форм + +Справочник типовых паттернов дизайна управляемых форм 1С. Вызывай **перед** проектированием формы через `/form-compile`, когда требования пользователя не детализируют расположение элементов. + +**Как использовать:** выбери подходящий архетип, применяй конвенции именования, при необходимости используй продвинутые паттерны. + +--- + +## Архетипы форм + +### Форма документа + +``` +Шапка (horizontal, 2 колонки) +├─ Левая (vertical): НомерДата (H: Номер + Дата "от"), Контрагент, Договор +├─ Правая (vertical): Организация, Подразделение, ЦеныИВалюта (надпись-ссылка) +Страницы (pages) +├─ Товары: таблица Объект.Товары +├─ Услуги: таблица Объект.Услуги (опционально) +└─ Дополнительно: прочие реквизиты +Подвал (vertical) +├─ Итоги (horizontal): Всего, НДС, Скидка +└─ КомментарийОтветственный (horizontal): Комментарий + Ответственный +``` + +**События:** OnCreateAtServer, OnReadAtServer, OnOpen, BeforeWriteAtServer, AfterWriteAtServer, AfterWrite, NotificationProcessing +**Свойства:** autoTitle=false + +### Форма обработки (DataProcessor) + +``` +Параметры (vertical) +├─ Группа полей ввода (Организация, Период, режимы работы) +├─ Информационные надписи (label, hyperlink) +Рабочая область +├─ Таблица данных или Pages с вкладками +Кнопки действий +├─ Выполнить / Применить (defaultButton) +├─ Закрыть (stdCommand: Close) +``` + +**События:** OnCreateAtServer, OnOpen, NotificationProcessing +**Свойства:** windowOpeningMode=LockOwnerWindow (если диалог), autoTitle=false + +### Форма списка + +``` +Отборы (group: alwaysHorizontal) +├─ ГруппаОтбор[Поле] (H): Флажок + Поле ввода (для каждого фильтра) +Список (table, DynamicList) +├─ Колонки: labelField (не input — данные только для чтения) +``` + +**События:** OnCreateAtServer, OnOpen, NotificationProcessing, OnLoadDataFromSettingsAtServer +**Свойства:** autoSaveDataInSettings=Use +**Фильтры:** пара реквизитов на каждый — `Отбор[Поле]` (значение) + `Отбор[Поле]Использование` (boolean) + +### Форма элемента справочника + +**Простая:** +``` +ГруппаРеквизитов (horizontal) +├─ Наименование -> Объект.Description +└─ Код -> Объект.Code (если нужен) +``` + +**Сложная:** +``` +Главное (vertical) +├─ Наименование -> Объект.Description +├─ Параметры (horizontal, 2 колонки) +│ ├─ Левая: основные реквизиты +│ └─ Правая: дополнительные реквизиты +└─ КонтактныеДанные / Дополнительно (vertical) +``` + +**События:** OnCreateAtServer, OnReadAtServer, BeforeWriteAtServer, NotificationProcessing + +### Мастер (Wizard) + +``` +Страницы (pages, OnCurrentPageChange) +├─ Шаг1: описание + параметры +├─ Шаг2: основная работа +└─ Шаг3: результат +Кнопки (horizontal) +├─ Назад (command), Далее (command, defaultButton), Выполнить (command) +└─ Закрыть (stdCommand: Close) +``` + +**Свойства:** windowOpeningMode=LockOwnerWindow, commandBarLocation=None + +--- + +## Конвенции именования + +### Группы + +| Назначение | Имя | Тип | +|-----------|-----|-----| +| Шапка | `ГруппаШапка` | horizontal | +| Левая колонка | `ГруппаШапкаЛевая` | vertical | +| Правая колонка | `ГруппаШапкаПравая` | vertical | +| Номер+Дата | `ГруппаНомерДата` | horizontal | +| Подвал | `ГруппаПодвал` | vertical | +| Итоги | `ГруппаИтоги` | horizontal | +| Кнопки | `ГруппаКнопок` | horizontal | +| Страницы | `ГруппаСтраницы` / `Страницы` | pages | +| Предупреждение | `ГруппаПредупреждение` | horizontal, visible:false | +| Доп. секция | `ГруппаДополнительно` / `ГруппаПрочее` | vertical, collapse | + +### Элементы + +| Назначение | Имя | +|-----------|-----| +| Поле в таблице | `[Таблица][Поле]` | +| Итог | `Итоги[Поле]` | +| Надпись-ссылка | `[Поле]Надпись` | +| Фильтр | `Отбор[Поле]` | +| Флажок фильтра | `Отбор[Поле]Использование` | +| Кнопка команды | `[Команда]Кнопка` | +| Баннер-картинка | `[Баннер]Картинка` | +| Баннер-надпись | `[Баннер]Надпись` | +| Подменю | `Подменю[Действие]` | + +### Обработчики событий + +Имя = имя элемента + суффикс на русском: + +| Событие | Суффикс | Пример | +|---------|---------|--------| +| OnChange | ПриИзменении | `ОрганизацияПриИзменении` | +| StartChoice | НачалоВыбора | `КонтрагентНачалоВыбора` | +| Click | Нажатие | `ЦеныИВалютаНажатие` | +| OnEditEnd | ПриОкончанииРедактирования | `ТоварыПриОкончанииРедактирования` | +| OnStartEdit | ПриНачалеРедактирования | `ТоварыПриНачалеРедактирования` | + +Обработчики формы: `ПриСозданииНаСервере`, `ПриОткрытии`, `ПередЗакрытием`, `ОбработкаОповещения`. + +--- + +## Принципы компоновки + +1. **Порядок чтения.** Сверху вниз, слева направо. Самое важное — вверху. +2. **Двухколоночная шапка.** Основные реквизиты слева (контрагент, склад), организационные справа (организация, подразделение). +3. **Кнопки действий внизу.** Главная кнопка — `defaultButton: true`. Закрыть — всегда последняя. +4. **Таблицы — основная область.** Табличные части занимают большую часть формы, обычно на Pages. +5. **Итоги рядом с таблицей.** В подвале, горизонтальная группа, все поля readOnly. +6. **Фильтры — отдельная зона.** Над списком, alwaysHorizontal, пара «флажок + поле» на каждый фильтр. +7. **Скрытые элементы для состояний.** Баннеры, предупреждения — `visible: false`, показываются программно. +8. **Надписи-ссылки для диалогов.** `labelField` с `hyperlink: true` и событием Click. + +--- + +## Продвинутые паттерны (ERP) + +### Сворачиваемые группы + +Для необязательных секций (подписи, дополнительно, прочее): + +```json +{ "group": "vertical", "name": "ГруппаПодписи", "title": "Подписи", + "behavior": "Collapsible", "collapsed": true, "children": [...] } +``` + +### Баннер-предупреждение + +Группа «картинка + надпись», скрыта по умолчанию, показывается программно: + +```json +{ "group": "horizontal", "name": "ГруппаПредупреждение", "showTitle": false, + "visible": false, "children": [ + { "picture": "ПредупреждениеКартинка" }, + { "label": "ПредупреждениеНадпись", "title": "Текст", "maxWidth": 76, "autoMaxWidth": false } +]} +``` + +### Popup-меню в командной панели + +Группировка связанных команд (печать, отправка) в одну кнопку с иконкой: + +```json +{ "cmdBar": "КоманднаяПанель", "children": [ + { "popup": "ПодменюПечать", "title": "Печать", + "picture": "StdPicture.Print", "representation": "Picture", "children": [ + { "button": "ПечатьНакладная", "command": "Печать" }, + { "button": "ПечатьСчёт", "command": "ПечатьСчёт" } + ]} +]} +``` + +### Форма без стандартной командной панели + +Для модальных диалогов и мастеров: + +```json +{ "properties": { "commandBarLocation": "None", "windowOpeningMode": "LockWholeInterface" } } +``` + +### Надпись-гиперссылка + +Вместо кнопки для открытия подформ (ЦеныИВалюта, УчётнаяПолитика): + +```json +{ "labelField": "ЦеныИВалютаНадпись", "path": "ЦеныИВалюта", "hyperlink": true, "on": ["Click"] } +``` + +--- + +## Пример: форма обработки (полный DSL) + +```json +{ + "title": "Загрузка данных из CSV", + "properties": { "autoTitle": false, "windowOpeningMode": "LockOwnerWindow" }, + "events": { "OnCreateAtServer": "ПриСозданииНаСервере" }, + "elements": [ + { "group": "vertical", "name": "ГруппаПараметры", "children": [ + { "input": "ФайлЗагрузки", "path": "ФайлЗагрузки", "title": "Файл", "clearButton": true, "horizontalStretch": true, "on": ["StartChoice"] }, + { "input": "Кодировка", "path": "Кодировка" }, + { "input": "Разделитель", "path": "Разделитель", "title": "Разделитель колонок" } + ]}, + { "table": "Данные", "path": "Объект.Данные", "on": ["OnStartEdit"], "columns": [ + { "input": "ДанныеНомерСтроки", "path": "Объект.Данные.LineNumber", "readOnly": true, "title": "№" }, + { "input": "ДанныеНаименование", "path": "Объект.Данные.Наименование" }, + { "input": "ДанныеКоличество", "path": "Объект.Данные.Количество", "on": ["OnChange"] }, + { "input": "ДанныеСумма", "path": "Объект.Данные.Сумма", "readOnly": true } + ]}, + { "group": "horizontal", "name": "ГруппаКнопок", "children": [ + { "button": "Загрузить", "command": "Загрузить", "title": "Загрузить из файла", "defaultButton": true }, + { "button": "Очистить", "command": "Очистить", "title": "Очистить таблицу" }, + { "button": "Закрыть", "stdCommand": "Close" } + ]} + ], + "attributes": [ + { "name": "Объект", "type": "ExternalDataProcessorObject.ЗагрузкаИзCSV", "main": true }, + { "name": "ФайлЗагрузки", "type": "string" }, + { "name": "Кодировка", "type": "string(20)" }, + { "name": "Разделитель", "type": "string(5)" } + ], + "commands": [ + { "name": "Загрузить", "action": "ЗагрузитьОбработка" }, + { "name": "Очистить", "action": "ОчиститьОбработка" } + ] +} +``` diff --git a/docs/form-guide.md b/docs/form-guide.md index e408cb75..4c3b7124 100644 --- a/docs/form-guide.md +++ b/docs/form-guide.md @@ -10,6 +10,7 @@ | `/form-compile` | `<JsonPath> <OutputPath>` | Генерация Form.xml из компактного JSON-определения | | `/form-validate` | `<FormPath>` | Валидация: уникальность ID, companions, DataPath, команды | | `/form-add` | `<FormPath> <JsonPath>` | Добавление элементов, реквизитов, команд в существующую форму | +| `/form-patterns` | (без параметров) | Справочник паттернов: архетипы, конвенции именования, продвинутые приёмы | ## Сценарии использования