mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-06-11 00:14:56 +03:00
feat(cfe,form): add borrowed form support across 6 skills
- cfe-borrow: borrow forms via Type.Name.Form.FormName, auto-borrow parent, generate Form.xml with BaseForm + metadata + empty Module.bsl - form-edit: formEvents, elementEvents, callType on events/commands, auto-detect extension mode (IDs 1000000+) - form-info: [EXTENSION] marker, callType on events/commands, BaseForm footer - form-validate: callType value checks, extension ID range warnings, BaseForm presence, callType-without-BaseForm detection - cfe-diff: form-level analysis in Mode A — borrowed/own forms, callType interceptors on events and commands - cfe-patch-method: warn if Form.xml missing for .Form. paths Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -30,9 +30,20 @@ allowed-tools:
|
||||
- `CommonModule.РаботаСФайлами` — общий модуль
|
||||
- `Document.РеализацияТоваров` — документ
|
||||
- `Enum.ВидыОплат` — перечисление
|
||||
- `Catalog.Контрагенты.Form.ФормаЭлемента` — форма объекта (заимствование формы)
|
||||
- `Catalog.X ;; CommonModule.Y ;; Enum.Z` — несколько объектов
|
||||
Поддерживаются все 44 типа объектов конфигурации.
|
||||
|
||||
### Заимствование форм
|
||||
|
||||
Формат `Тип.Имя.Form.ИмяФормы` заимствует форму конкретного объекта. Если родительский объект ещё не заимствован — он будет заимствован автоматически.
|
||||
|
||||
Создаётся:
|
||||
1. **Метаданные формы** — `Forms/ИмяФормы.xml` с `ObjectBelonging=Adopted`, `FormType=Managed`
|
||||
2. **Form.xml** — `Forms/ИмяФормы/Ext/Form.xml` с копией исходной формы + `<BaseForm>` (начальное состояние)
|
||||
3. **Module.bsl** — пустой файл `Forms/ИмяФормы/Ext/Form/Module.bsl`
|
||||
4. **Регистрация** — `<Form>` в ChildObjects родительского объекта
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
@@ -45,6 +56,9 @@ powershell.exe -NoProfile -File .claude/skills/cfe-borrow/scripts/cfe-borrow.ps1
|
||||
# Заимствовать один объект
|
||||
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты"
|
||||
|
||||
# Заимствовать форму (автоматически заимствует родительский объект)
|
||||
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты.Form.ФормаЭлемента"
|
||||
|
||||
# Несколько объектов за раз
|
||||
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты ;; CommonModule.ОбщийМодуль ;; Enum.ВидыОплат"
|
||||
```
|
||||
|
||||
@@ -384,6 +384,239 @@ function Read-SourceObject {
|
||||
}
|
||||
}
|
||||
|
||||
# --- 10b. Helper: read source form UUID ---
|
||||
function Read-SourceFormUuid {
|
||||
param([string]$typeName, [string]$objName, [string]$formName)
|
||||
|
||||
$dirName = $childTypeDirMap[$typeName]
|
||||
$srcFile = Join-Path (Join-Path (Join-Path (Join-Path $cfgDir $dirName) $objName) "Forms") "${formName}.xml"
|
||||
if (-not (Test-Path $srcFile)) {
|
||||
Write-Error "Source form not found: $srcFile"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$srcDoc = New-Object System.Xml.XmlDocument
|
||||
$srcDoc.PreserveWhitespace = $false
|
||||
$srcDoc.Load($srcFile)
|
||||
|
||||
$srcEl = $null
|
||||
foreach ($c in $srcDoc.DocumentElement.ChildNodes) {
|
||||
if ($c.NodeType -eq 'Element') { $srcEl = $c; break }
|
||||
}
|
||||
if (-not $srcEl) {
|
||||
Write-Error "No metadata element found in source form: $srcFile"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$srcUuid = $srcEl.GetAttribute("uuid")
|
||||
if (-not $srcUuid) {
|
||||
Write-Error "No uuid attribute on source form element: $srcFile"
|
||||
exit 1
|
||||
}
|
||||
|
||||
return $srcUuid
|
||||
}
|
||||
|
||||
# --- 10c. Helper: borrow a form ---
|
||||
function Borrow-Form {
|
||||
param([string]$typeName, [string]$objName, [string]$formName)
|
||||
|
||||
$dirName = $childTypeDirMap[$typeName]
|
||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
||||
|
||||
# 1. Read source form UUID
|
||||
$formUuid = Read-SourceFormUuid $typeName $objName $formName
|
||||
Info " Source form UUID: $formUuid"
|
||||
|
||||
# 2. Read source Form.xml content
|
||||
$srcFormXmlPath = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $cfgDir $dirName) $objName) "Forms") $formName) "Ext/Form.xml"
|
||||
if (-not (Test-Path $srcFormXmlPath)) {
|
||||
Write-Error "Source Form.xml not found: $srcFormXmlPath"
|
||||
exit 1
|
||||
}
|
||||
$srcFormContent = [System.IO.File]::ReadAllText($srcFormXmlPath, $enc)
|
||||
|
||||
# 3. Generate form metadata XML (ФормаЭлемента.xml)
|
||||
$newFormUuid = [guid]::NewGuid().ToString()
|
||||
$formMetaSb = New-Object System.Text.StringBuilder
|
||||
$formMetaSb.AppendLine("<?xml version=`"1.0`" encoding=`"UTF-8`"?>") | Out-Null
|
||||
$formMetaSb.AppendLine("<MetaDataObject $($script:xmlnsDecl) version=`"2.17`">") | Out-Null
|
||||
$formMetaSb.AppendLine("`t<Form uuid=`"${newFormUuid}`">") | Out-Null
|
||||
$formMetaSb.AppendLine("`t`t<InternalInfo/>") | Out-Null
|
||||
$formMetaSb.AppendLine("`t`t<Properties>") | Out-Null
|
||||
$formMetaSb.AppendLine("`t`t`t<ObjectBelonging>Adopted</ObjectBelonging>") | Out-Null
|
||||
$formMetaSb.AppendLine("`t`t`t<Name>${formName}</Name>") | Out-Null
|
||||
$formMetaSb.AppendLine("`t`t`t<Comment/>") | Out-Null
|
||||
$formMetaSb.AppendLine("`t`t`t<ExtendedConfigurationObject>${formUuid}</ExtendedConfigurationObject>") | Out-Null
|
||||
$formMetaSb.AppendLine("`t`t`t<FormType>Managed</FormType>") | Out-Null
|
||||
$formMetaSb.AppendLine("`t`t</Properties>") | Out-Null
|
||||
$formMetaSb.AppendLine("`t</Form>") | Out-Null
|
||||
$formMetaSb.Append("</MetaDataObject>") | Out-Null
|
||||
|
||||
# 4. Create directories
|
||||
$formMetaDir = Join-Path (Join-Path (Join-Path $extDir $dirName) $objName) "Forms"
|
||||
if (-not (Test-Path $formMetaDir)) {
|
||||
New-Item -ItemType Directory -Path $formMetaDir -Force | Out-Null
|
||||
}
|
||||
|
||||
# Write form metadata
|
||||
$formMetaFile = Join-Path $formMetaDir "${formName}.xml"
|
||||
[System.IO.File]::WriteAllText($formMetaFile, $formMetaSb.ToString(), $enc)
|
||||
Info " Created: $formMetaFile"
|
||||
|
||||
# 5. Generate Form.xml with BaseForm
|
||||
# Extract inner content from source (everything between <Form ...> and </Form>)
|
||||
$innerContent = ""
|
||||
$formVersion = "2.17"
|
||||
if ($srcFormContent -match '(?s)<Form[^>]*version="([^"]*)"[^>]*>(.*)</Form>') {
|
||||
$formVersion = $Matches[1]
|
||||
$innerContent = $Matches[2]
|
||||
} elseif ($srcFormContent -match '(?s)<Form[^>]*>(.*)</Form>') {
|
||||
$innerContent = $Matches[1]
|
||||
}
|
||||
|
||||
# Build the extension Form.xml: resultant form + <BaseForm>
|
||||
$formXmlSb = New-Object System.Text.StringBuilder
|
||||
# Copy the original XML declaration and <Form> opening tag
|
||||
if ($srcFormContent -match '(?s)^(.*?<Form[^>]*>)') {
|
||||
$formXmlSb.Append($Matches[1]) | Out-Null
|
||||
}
|
||||
# Resultant form content (same as source initially)
|
||||
$formXmlSb.Append($innerContent) | Out-Null
|
||||
# BaseForm section
|
||||
$formXmlSb.AppendLine("`t<BaseForm version=`"${formVersion}`">") | Out-Null
|
||||
# Inner content for BaseForm (trim leading newline)
|
||||
$baseInner = $innerContent.TrimStart("`r", "`n")
|
||||
$formXmlSb.Append("`t") | Out-Null
|
||||
$formXmlSb.Append($baseInner) | Out-Null
|
||||
# Close BaseForm — ensure it's on its own line
|
||||
$lastChar = $formXmlSb.ToString()[-1]
|
||||
if ($lastChar -ne "`n") { $formXmlSb.AppendLine() | Out-Null }
|
||||
$formXmlSb.AppendLine("`t</BaseForm>") | Out-Null
|
||||
$formXmlSb.Append("</Form>") | Out-Null
|
||||
|
||||
# Write Form.xml
|
||||
$formXmlDir = Join-Path (Join-Path $formMetaDir $formName) "Ext"
|
||||
if (-not (Test-Path $formXmlDir)) {
|
||||
New-Item -ItemType Directory -Path $formXmlDir -Force | Out-Null
|
||||
}
|
||||
$formXmlFile = Join-Path $formXmlDir "Form.xml"
|
||||
[System.IO.File]::WriteAllText($formXmlFile, $formXmlSb.ToString(), $enc)
|
||||
Info " Created: $formXmlFile"
|
||||
|
||||
# 6. Create empty Module.bsl
|
||||
$moduleDir = Join-Path $formXmlDir "Form"
|
||||
if (-not (Test-Path $moduleDir)) {
|
||||
New-Item -ItemType Directory -Path $moduleDir -Force | Out-Null
|
||||
}
|
||||
$moduleBslFile = Join-Path $moduleDir "Module.bsl"
|
||||
[System.IO.File]::WriteAllText($moduleBslFile, "", $enc)
|
||||
Info " Created: $moduleBslFile"
|
||||
|
||||
# 7. Register form in parent object ChildObjects
|
||||
Register-FormInObject $typeName $objName $formName
|
||||
|
||||
return @($formMetaFile, $formXmlFile, $moduleBslFile)
|
||||
}
|
||||
|
||||
# --- 10d. Helper: register form in parent object's ChildObjects ---
|
||||
function Register-FormInObject {
|
||||
param([string]$typeName, [string]$objName, [string]$formName)
|
||||
|
||||
$dirName = $childTypeDirMap[$typeName]
|
||||
$objFile = Join-Path (Join-Path $extDir $dirName) "${objName}.xml"
|
||||
|
||||
if (-not (Test-Path $objFile)) {
|
||||
Warn "Parent object file not found: $objFile — form not registered in ChildObjects"
|
||||
return
|
||||
}
|
||||
|
||||
$objDoc = New-Object System.Xml.XmlDocument
|
||||
$objDoc.PreserveWhitespace = $true
|
||||
$objDoc.Load($objFile)
|
||||
|
||||
$objNs = New-Object System.Xml.XmlNamespaceManager($objDoc.NameTable)
|
||||
$objNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
||||
|
||||
# Find the type element
|
||||
$objEl = $null
|
||||
foreach ($c in $objDoc.DocumentElement.ChildNodes) {
|
||||
if ($c.NodeType -eq 'Element') { $objEl = $c; break }
|
||||
}
|
||||
if (-not $objEl) {
|
||||
Warn "No type element in $objFile — form not registered"
|
||||
return
|
||||
}
|
||||
|
||||
# Find or create ChildObjects
|
||||
$childObjs = $objEl.SelectSingleNode("md:ChildObjects", $objNs)
|
||||
if (-not $childObjs) {
|
||||
# Create ChildObjects element
|
||||
$childObjs = $objDoc.CreateElement("ChildObjects", "http://v8.1c.ru/8.3/MDClasses")
|
||||
$objEl.AppendChild($objDoc.CreateWhitespace("`r`n`t`t")) | Out-Null
|
||||
$objEl.AppendChild($childObjs) | Out-Null
|
||||
$objEl.AppendChild($objDoc.CreateWhitespace("`r`n`t")) | Out-Null
|
||||
}
|
||||
|
||||
# Check dedup
|
||||
foreach ($c in $childObjs.ChildNodes) {
|
||||
if ($c.NodeType -eq 'Element' -and $c.LocalName -eq "Form" -and $c.InnerText -eq $formName) {
|
||||
Warn "Form '$formName' already in ChildObjects of ${typeName}.${objName}"
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
# Expand self-closing if needed
|
||||
if (-not $childObjs.HasChildNodes -or $childObjs.IsEmpty) {
|
||||
$closeWs = $objDoc.CreateWhitespace("`r`n`t`t")
|
||||
$childObjs.AppendChild($closeWs) | Out-Null
|
||||
}
|
||||
|
||||
# Add <Form>formName</Form>
|
||||
$formEl = $objDoc.CreateElement("Form", "http://v8.1c.ru/8.3/MDClasses")
|
||||
$formEl.InnerText = $formName
|
||||
|
||||
$trailing = $childObjs.LastChild
|
||||
$ws = $objDoc.CreateWhitespace("`r`n`t`t`t")
|
||||
if ($trailing -and ($trailing.NodeType -eq 'Whitespace' -or $trailing.NodeType -eq 'SignificantWhitespace')) {
|
||||
$childObjs.InsertBefore($ws, $trailing) | Out-Null
|
||||
$childObjs.InsertBefore($formEl, $trailing) | Out-Null
|
||||
} else {
|
||||
$childObjs.AppendChild($ws) | Out-Null
|
||||
$childObjs.AppendChild($formEl) | Out-Null
|
||||
}
|
||||
|
||||
# Save object XML
|
||||
$settings2 = New-Object System.Xml.XmlWriterSettings
|
||||
$settings2.Encoding = New-Object System.Text.UTF8Encoding($true)
|
||||
$settings2.Indent = $false
|
||||
$settings2.NewLineHandling = [System.Xml.NewLineHandling]::None
|
||||
|
||||
$memStream2 = New-Object System.IO.MemoryStream
|
||||
$writer2 = [System.Xml.XmlWriter]::Create($memStream2, $settings2)
|
||||
$objDoc.Save($writer2)
|
||||
$writer2.Flush(); $writer2.Close()
|
||||
|
||||
$bytes2 = $memStream2.ToArray()
|
||||
$memStream2.Close()
|
||||
$text2 = [System.Text.Encoding]::UTF8.GetString($bytes2)
|
||||
if ($text2.Length -gt 0 -and $text2[0] -eq [char]0xFEFF) { $text2 = $text2.Substring(1) }
|
||||
$text2 = $text2.Replace('encoding="utf-8"', 'encoding="UTF-8"')
|
||||
|
||||
$utf8Bom2 = New-Object System.Text.UTF8Encoding($true)
|
||||
[System.IO.File]::WriteAllText($objFile, $text2, $utf8Bom2)
|
||||
Info " Registered form in: $objFile"
|
||||
}
|
||||
|
||||
# --- 10e. Helper: check if object is already borrowed in extension ---
|
||||
function Test-ObjectBorrowed {
|
||||
param([string]$typeName, [string]$objName)
|
||||
|
||||
$dirName = $childTypeDirMap[$typeName]
|
||||
$objFile = Join-Path (Join-Path $extDir $dirName) "${objName}.xml"
|
||||
return (Test-Path $objFile)
|
||||
}
|
||||
|
||||
# --- 11. Helper: generate InternalInfo XML ---
|
||||
function Build-InternalInfoXml {
|
||||
param([string]$typeName, [string]$objName, [string]$indent)
|
||||
@@ -534,11 +767,11 @@ $borrowedCount = 0
|
||||
foreach ($item in $items) {
|
||||
$dotIdx = $item.IndexOf(".")
|
||||
if ($dotIdx -lt 1) {
|
||||
Write-Error "Invalid format '${item}', expected 'Type.Name'"
|
||||
Write-Error "Invalid format '${item}', expected 'Type.Name' or 'Type.Name.Form.FormName'"
|
||||
exit 1
|
||||
}
|
||||
$typeName = $item.Substring(0, $dotIdx)
|
||||
$objName = $item.Substring($dotIdx + 1)
|
||||
$remainder = $item.Substring($dotIdx + 1)
|
||||
|
||||
# Resolve Russian synonym to English type name
|
||||
if ($synonymMap.ContainsKey($typeName)) { $typeName = $synonymMap[$typeName] }
|
||||
@@ -548,34 +781,71 @@ foreach ($item in $items) {
|
||||
exit 1
|
||||
}
|
||||
|
||||
$dirName = $childTypeDirMap[$typeName]
|
||||
|
||||
Info "Borrowing ${typeName}.${objName}..."
|
||||
|
||||
# Read source object
|
||||
$src = Read-SourceObject $typeName $objName
|
||||
Info " Source UUID: $($src.Uuid)"
|
||||
|
||||
# Build borrowed object XML
|
||||
$borrowedXml = Build-BorrowedObjectXml $typeName $objName $src.Uuid $src.Properties
|
||||
|
||||
# Create directory in extension if needed
|
||||
$targetDir = Join-Path $extDir $dirName
|
||||
if (-not (Test-Path $targetDir)) {
|
||||
New-Item -ItemType Directory -Path $targetDir -Force | Out-Null
|
||||
# Check for .Form. pattern: Type.ObjName.Form.FormName
|
||||
$formName = $null
|
||||
$formIdx = $remainder.IndexOf(".Form.")
|
||||
if ($formIdx -gt 0) {
|
||||
$objName = $remainder.Substring(0, $formIdx)
|
||||
$formName = $remainder.Substring($formIdx + 6) # skip ".Form."
|
||||
} else {
|
||||
$objName = $remainder
|
||||
}
|
||||
|
||||
# Write borrowed object XML with UTF-8 BOM
|
||||
$targetFile = Join-Path $targetDir "${objName}.xml"
|
||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
||||
[System.IO.File]::WriteAllText($targetFile, $borrowedXml, $enc)
|
||||
Info " Created: $targetFile"
|
||||
$dirName = $childTypeDirMap[$typeName]
|
||||
|
||||
# Add to ChildObjects
|
||||
Add-ToChildObjects $typeName $objName
|
||||
if ($formName) {
|
||||
# --- Form borrowing ---
|
||||
Info "Borrowing form ${typeName}.${objName}.Form.${formName}..."
|
||||
|
||||
$borrowedFiles += $targetFile
|
||||
$borrowedCount++
|
||||
# Auto-borrow parent object if not yet borrowed
|
||||
if (-not (Test-ObjectBorrowed $typeName $objName)) {
|
||||
Info " Parent object ${typeName}.${objName} not yet borrowed — borrowing first..."
|
||||
|
||||
$src = Read-SourceObject $typeName $objName
|
||||
Info " Source UUID: $($src.Uuid)"
|
||||
$borrowedXml = Build-BorrowedObjectXml $typeName $objName $src.Uuid $src.Properties
|
||||
|
||||
$targetDir = Join-Path $extDir $dirName
|
||||
if (-not (Test-Path $targetDir)) {
|
||||
New-Item -ItemType Directory -Path $targetDir -Force | Out-Null
|
||||
}
|
||||
$targetFile = Join-Path $targetDir "${objName}.xml"
|
||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
||||
[System.IO.File]::WriteAllText($targetFile, $borrowedXml, $enc)
|
||||
Info " Created: $targetFile"
|
||||
|
||||
Add-ToChildObjects $typeName $objName
|
||||
$borrowedFiles += $targetFile
|
||||
}
|
||||
|
||||
# Borrow the form
|
||||
$formFiles = Borrow-Form $typeName $objName $formName
|
||||
$borrowedFiles += $formFiles
|
||||
$borrowedCount++
|
||||
} else {
|
||||
# --- Object borrowing (existing logic) ---
|
||||
Info "Borrowing ${typeName}.${objName}..."
|
||||
|
||||
$src = Read-SourceObject $typeName $objName
|
||||
Info " Source UUID: $($src.Uuid)"
|
||||
|
||||
$borrowedXml = Build-BorrowedObjectXml $typeName $objName $src.Uuid $src.Properties
|
||||
|
||||
$targetDir = Join-Path $extDir $dirName
|
||||
if (-not (Test-Path $targetDir)) {
|
||||
New-Item -ItemType Directory -Path $targetDir -Force | Out-Null
|
||||
}
|
||||
|
||||
$targetFile = Join-Path $targetDir "${objName}.xml"
|
||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
||||
[System.IO.File]::WriteAllText($targetFile, $borrowedXml, $enc)
|
||||
Info " Created: $targetFile"
|
||||
|
||||
Add-ToChildObjects $typeName $objName
|
||||
|
||||
$borrowedFiles += $targetFile
|
||||
$borrowedCount++
|
||||
}
|
||||
}
|
||||
|
||||
# --- 15. Save modified Configuration.xml ---
|
||||
|
||||
@@ -38,9 +38,18 @@ powershell.exe -NoProfile -File .claude/skills/cfe-diff/scripts/cfe-diff.ps1 -Ex
|
||||
&ИзменениеИКонтроль("РеквизитыРедактируемыеВГрупповойОбработке") — line 4 in ...
|
||||
&Перед("ЗагрузитьКурсыВалют") — line 13 in ...
|
||||
ChildObjects: 1 own attrs, 1 own TS, 3 own forms
|
||||
Form.ФормаЭлемента (borrowed):
|
||||
Event:OnCreateAtServer [After] -> Расш1_ПриСозданииПосле
|
||||
Command:Подбор [Before] -> Расш1_ПодборПеред
|
||||
Form.Расш1_МояФорма (own)
|
||||
[OWN] Catalog.Расш5_Справочник1
|
||||
```
|
||||
|
||||
Для каждой формы заимствованного объекта показывается:
|
||||
- `(borrowed)` / `(own)` — заимствованная или собственная форма
|
||||
- callType-события формы и элементов
|
||||
- callType на командах
|
||||
|
||||
## Mode B — проверка переноса
|
||||
|
||||
Для каждого `&ИзменениеИКонтроль` извлекает блоки `#Вставка`/`#КонецВставки` из расширения и ищет их в соответствующем модуле конфигурации.
|
||||
|
||||
@@ -210,6 +210,63 @@ function Get-InsertionBlocks {
|
||||
return $blocks
|
||||
}
|
||||
|
||||
# --- Helper: analyze form for callType events and commands ---
|
||||
function Get-FormInterceptors {
|
||||
param([string]$formXmlPath)
|
||||
|
||||
if (-not (Test-Path $formXmlPath)) { return $null }
|
||||
|
||||
$formDoc = New-Object System.Xml.XmlDocument
|
||||
$formDoc.PreserveWhitespace = $false
|
||||
try { $formDoc.Load($formXmlPath) } catch { return $null }
|
||||
|
||||
$fNs = New-Object System.Xml.XmlNamespaceManager($formDoc.NameTable)
|
||||
$fNs.AddNamespace("f", "http://v8.1c.ru/8.3/xcf/logform")
|
||||
|
||||
$fRoot = $formDoc.DocumentElement
|
||||
$baseForm = $fRoot.SelectSingleNode("f:BaseForm", $fNs)
|
||||
$isBorrowed = ($baseForm -ne $null)
|
||||
|
||||
$interceptors = @()
|
||||
|
||||
# Form-level events with callType
|
||||
$eventsNode = $fRoot.SelectSingleNode("f:Events", $fNs)
|
||||
if ($eventsNode) {
|
||||
foreach ($evt in $eventsNode.SelectNodes("f:Event", $fNs)) {
|
||||
$ct = $evt.GetAttribute("callType")
|
||||
if ($ct) {
|
||||
$interceptors += "Event:$($evt.GetAttribute('name')) [$ct] -> $($evt.InnerText)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Element-level events with callType (scan all elements recursively)
|
||||
$childItems = $fRoot.SelectSingleNode("f:ChildItems", $fNs)
|
||||
if ($childItems) {
|
||||
foreach ($evtNode in $childItems.SelectNodes(".//*[f:Events/f:Event[@callType]]", $fNs)) {
|
||||
$elName = $evtNode.GetAttribute("name")
|
||||
foreach ($evt in $evtNode.SelectNodes("f:Events/f:Event[@callType]", $fNs)) {
|
||||
$ct = $evt.GetAttribute("callType")
|
||||
$interceptors += "Element:${elName}.$($evt.GetAttribute('name')) [$ct] -> $($evt.InnerText)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Commands with callType on Action
|
||||
foreach ($cmd in $fRoot.SelectNodes("f:Commands/f:Command", $fNs)) {
|
||||
$cmdName = $cmd.GetAttribute("name")
|
||||
foreach ($action in $cmd.SelectNodes("f:Action[@callType]", $fNs)) {
|
||||
$ct = $action.GetAttribute("callType")
|
||||
$interceptors += "Command:$cmdName [$ct] -> $($action.InnerText)"
|
||||
}
|
||||
}
|
||||
|
||||
return @{
|
||||
IsBorrowed = $isBorrowed
|
||||
Interceptors = $interceptors
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# MODE A: Extension overview
|
||||
# ============================================================
|
||||
@@ -255,6 +312,7 @@ if ($Mode -eq "A") {
|
||||
$ownForms = 0
|
||||
$ownTS = 0
|
||||
$borrowedItems = 0
|
||||
$formNames = @()
|
||||
foreach ($c in $childObj.ChildNodes) {
|
||||
if ($c.NodeType -ne 'Element') { continue }
|
||||
$cProps = $c.SelectSingleNode("md:Properties", $info.ObjNs)
|
||||
@@ -268,7 +326,7 @@ if ($Mode -eq "A") {
|
||||
switch ($c.LocalName) {
|
||||
"Attribute" { $ownAttrs++ }
|
||||
"TabularSection" { $ownTS++ }
|
||||
"Form" { $ownForms++ }
|
||||
"Form" { $formNames += $c.InnerText; $ownForms++ }
|
||||
}
|
||||
}
|
||||
$parts = @()
|
||||
@@ -279,6 +337,27 @@ if ($Mode -eq "A") {
|
||||
if ($parts.Count -gt 0) {
|
||||
Write-Host " ChildObjects: $($parts -join ', ')"
|
||||
}
|
||||
|
||||
# Analyze forms
|
||||
$borrowedFormCount = 0
|
||||
$ownFormCount = 0
|
||||
foreach ($fn in $formNames) {
|
||||
$formXmlPath = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $ExtensionPath $info.DirName) $info.Name) "Forms") $fn) "Ext/Form.xml"
|
||||
$fi = Get-FormInterceptors $formXmlPath
|
||||
if (-not $fi) {
|
||||
Write-Host " Form.$fn (?)"
|
||||
continue
|
||||
}
|
||||
$formTag = if ($fi.IsBorrowed) { "borrowed"; $borrowedFormCount++ } else { "own"; $ownFormCount++ }
|
||||
if ($fi.Interceptors.Count -gt 0) {
|
||||
Write-Host " Form.$fn ($formTag):"
|
||||
foreach ($ic in $fi.Interceptors) {
|
||||
Write-Host " $ic"
|
||||
}
|
||||
} else {
|
||||
Write-Host " Form.$fn ($formTag)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -157,6 +157,21 @@ $bslCode += "$endKeyword"
|
||||
|
||||
$bslText = ($bslCode -join "`r`n") + "`r`n"
|
||||
|
||||
# --- Check form borrowing for .Form. paths ---
|
||||
if ($parts.Count -ge 4 -and $parts[2] -eq "Form") {
|
||||
$formName = $parts[3]
|
||||
$dirName = $typeDirMap[$objType]
|
||||
$formMetaFile = Join-Path (Join-Path (Join-Path (Join-Path $ExtensionPath $dirName) $objName) "Forms") "${formName}.xml"
|
||||
$formXmlFile = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $ExtensionPath $dirName) $objName) "Forms") $formName) "Ext/Form.xml"
|
||||
|
||||
if (-not (Test-Path $formMetaFile) -or -not (Test-Path $formXmlFile)) {
|
||||
Write-Host "[WARN] Form '$formName' metadata or Form.xml not found in extension."
|
||||
Write-Host " Run /cfe-borrow first:"
|
||||
Write-Host " /cfe-borrow -ExtensionPath $ExtensionPath -ConfigPath <ConfigPath> -Object `"$objType.$objName.Form.$formName`""
|
||||
Write-Host ""
|
||||
}
|
||||
}
|
||||
|
||||
# --- Check if file exists and append ---
|
||||
$bslDir = Split-Path $bslFile -Parent
|
||||
if (-not (Test-Path $bslDir)) {
|
||||
|
||||
@@ -50,6 +50,32 @@ powershell.exe -NoProfile -File .claude/skills/form-edit/scripts/form-edit.ps1 -
|
||||
}
|
||||
```
|
||||
|
||||
### Расширения (extension-формы)
|
||||
|
||||
Для заимствованных форм (с `<BaseForm>`) автоматически активируется extension-режим: ID начинаются с 1000000+. Доступны дополнительные секции:
|
||||
|
||||
```json
|
||||
{
|
||||
"formEvents": [
|
||||
{ "name": "OnCreateAtServer", "handler": "Расш1_ПриСозданииПосле", "callType": "After" },
|
||||
{ "name": "OnOpen", "handler": "Расш1_ПриОткрытии", "callType": "Before" }
|
||||
],
|
||||
"elementEvents": [
|
||||
{ "element": "Банк", "name": "OnChange", "handler": "Расш1_БанкПриИзменении", "callType": "Before" }
|
||||
],
|
||||
"commands": [
|
||||
{ "name": "Подбор", "action": "Расш1_ПодборПосле", "callType": "After" },
|
||||
{ "name": "Запрос", "actions": [
|
||||
{ "callType": "Before", "handler": "Расш1_ЗапросПеред" },
|
||||
{ "callType": "After", "handler": "Расш1_ЗапросПосле" }
|
||||
]}
|
||||
],
|
||||
"elements": [
|
||||
{ "input": "Поле", "path": "Объект.Поле", "on": [{ "event": "OnChange", "callType": "After" }] }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Позиционирование элементов
|
||||
|
||||
| Ключ | По умолчанию | Описание |
|
||||
@@ -96,19 +122,35 @@ powershell.exe -NoProfile -File .claude/skills/form-edit/scripts/form-edit.ps1 -
|
||||
|
||||
`string`, `string(100)`, `decimal(15,2)`, `boolean`, `date`, `dateTime`, `CatalogRef.XXX`, `DocumentObject.XXX`, `ValueTable`, `DynamicList`, `Type1 | Type2` (составной).
|
||||
|
||||
### Секции расширений
|
||||
|
||||
| Секция | Назначение |
|
||||
|--------|-----------|
|
||||
| `formEvents` | События уровня формы с `callType` (Before/After/Override) |
|
||||
| `elementEvents` | События на существующих элементах заимствованной формы |
|
||||
| `callType` на `commands` | callType на Action команды |
|
||||
| `callType` на `on` | callType на событиях новых элементов (объектный формат) |
|
||||
|
||||
Все extension-секции опциональны — без них навык работает как с обычными формами.
|
||||
|
||||
## Вывод
|
||||
|
||||
```
|
||||
=== form-edit: Форма ===
|
||||
|
||||
[EXTENSION] BaseForm detected — IDs start at 1000000+
|
||||
|
||||
Added form events:
|
||||
+ OnCreateAtServer[After] -> Расш1_ПриСозданииПосле
|
||||
|
||||
Added elements (into ГруппаШапка, after Контрагент):
|
||||
+ [Input] Склад -> Объект.Склад {OnChange}
|
||||
|
||||
Added attributes:
|
||||
+ СуммаИтого: decimal(15,2) (id=12)
|
||||
+ СуммаИтого: decimal(15,2) (id=1000000)
|
||||
|
||||
---
|
||||
Total: 1 element(s) (+2 companions), 1 attribute(s)
|
||||
Total: 1 form event(s), 1 element(s) (+2 companions), 1 attribute(s)
|
||||
Run /form-validate to verify.
|
||||
```
|
||||
|
||||
|
||||
@@ -110,6 +110,16 @@ $script:nextElemId++
|
||||
$script:nextAttrId++
|
||||
$script:nextCmdId++
|
||||
|
||||
# --- 4b. Auto-detect extension mode (BaseForm present) ---
|
||||
$script:isExtension = $false
|
||||
$baseForm = $root.SelectSingleNode("f:BaseForm", $nsMgr)
|
||||
if ($baseForm) {
|
||||
$script:isExtension = $true
|
||||
if ($script:nextAttrId -lt 1000000) { $script:nextAttrId = 1000000 }
|
||||
if ($script:nextCmdId -lt 1000000) { $script:nextCmdId = 1000000 }
|
||||
if ($script:nextElemId -lt 1000000) { $script:nextElemId = 1000000 }
|
||||
}
|
||||
|
||||
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 }
|
||||
@@ -260,18 +270,29 @@ function Emit-Events {
|
||||
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 ', ')"
|
||||
$evtStr = if ($evt -is [string]) { "$evt" } else { "$($evt.event)" }
|
||||
if ($allowed.Count -gt 0 -and $allowed -notcontains $evtStr) {
|
||||
Write-Host "[WARN] Unknown event '$evtStr' for $typeKey '$elementName'. Known: $($allowed -join ', ')"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
X "$indent<Events>"
|
||||
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<Event name=`"$evtName`">$handler</Event>"
|
||||
# Support both string ("OnChange") and object ({ "event": "OnChange", "callType": "After" })
|
||||
if ($evt -is [string] -or -not $evt.event) {
|
||||
$evtName = "$evt"
|
||||
$handler = if ($el.handlers -and $el.handlers.$evtName) { "$($el.handlers.$evtName)" }
|
||||
else { Get-HandlerName -elementName $elementName -eventName $evtName }
|
||||
X "$indent`t<Event name=`"$evtName`">$handler</Event>"
|
||||
} else {
|
||||
$evtName = "$($evt.event)"
|
||||
$handler = if ($evt.handler) { "$($evt.handler)" }
|
||||
elseif ($el.handlers -and $el.handlers.$evtName) { "$($el.handlers.$evtName)" }
|
||||
else { Get-HandlerName -elementName $elementName -eventName $evtName }
|
||||
$callTypeAttr = if ($evt.callType) { " callType=`"$($evt.callType)`"" } else { "" }
|
||||
X "$indent`t<Event name=`"$evtName`"$callTypeAttr>$handler</Event>"
|
||||
}
|
||||
}
|
||||
X "$indent</Events>"
|
||||
}
|
||||
@@ -964,7 +985,20 @@ if ($def.commands -and $def.commands.Count -gt 0) {
|
||||
$inner = "$cmdChildIndent`t"
|
||||
|
||||
if ($cmd.title) { Emit-MLText -tag "Title" -text "$($cmd.title)" -indent $inner }
|
||||
if ($cmd.action) { X "$inner<Action>$($cmd.action)</Action>" }
|
||||
|
||||
# Support single action with optional callType, or multiple actions
|
||||
if ($cmd.actions) {
|
||||
# Multiple actions: [{ "callType": "Before", "handler": "..." }, ...]
|
||||
foreach ($act in $cmd.actions) {
|
||||
$actHandler = "$($act.handler)"
|
||||
$callTypeAttr = if ($act.callType) { " callType=`"$($act.callType)`"" } else { "" }
|
||||
X "$inner<Action$callTypeAttr>$actHandler</Action>"
|
||||
}
|
||||
} elseif ($cmd.action) {
|
||||
$callTypeAttr = if ($cmd.callType) { " callType=`"$($cmd.callType)`"" } else { "" }
|
||||
X "$inner<Action$callTypeAttr>$($cmd.action)</Action>"
|
||||
}
|
||||
|
||||
if ($cmd.shortcut) { X "$inner<Shortcut>$($cmd.shortcut)</Shortcut>" }
|
||||
if ($cmd.picture) {
|
||||
X "$inner<Picture>"
|
||||
@@ -975,7 +1009,7 @@ if ($def.commands -and $def.commands.Count -gt 0) {
|
||||
if ($cmd.representation) { X "$inner<Representation>$($cmd.representation)</Representation>" }
|
||||
|
||||
X "$cmdChildIndent</Command>"
|
||||
$actionStr = if ($cmd.action) { " -> $($cmd.action)" } else { "" }
|
||||
$actionStr = if ($cmd.action) { " -> $($cmd.action)" } elseif ($cmd.actions) { " -> $($cmd.actions.Count) action(s)" } else { "" }
|
||||
$addedCmds += " + ${cmdName}${actionStr} (id=$cmdId)"
|
||||
}
|
||||
X "</_F>"
|
||||
@@ -988,6 +1022,127 @@ if ($def.commands -and $def.commands.Count -gt 0) {
|
||||
}
|
||||
}
|
||||
|
||||
# === 12b. Add form-level events ===
|
||||
|
||||
$addedFormEvents = @()
|
||||
|
||||
if ($def.formEvents -and $def.formEvents.Count -gt 0) {
|
||||
$eventsSection = $root.SelectSingleNode("f:Events", $nsMgr)
|
||||
if (-not $eventsSection) {
|
||||
# Create Events section — insert after AutoCommandBar or at the beginning
|
||||
$eventsSection = $xmlDoc.CreateElement("Events", $formNs)
|
||||
$insertAfter = $root.SelectSingleNode("f:AutoCommandBar", $nsMgr)
|
||||
if ($insertAfter) {
|
||||
$refNode = $insertAfter
|
||||
$ws = $xmlDoc.CreateWhitespace("`r`n`t")
|
||||
# Insert before the AutoCommandBar (Events come before AutoCommandBar in 1C)
|
||||
$root.InsertBefore($ws, $refNode) | Out-Null
|
||||
$root.InsertBefore($eventsSection, $refNode) | Out-Null
|
||||
} else {
|
||||
$firstChild = $root.FirstChild
|
||||
if ($firstChild) {
|
||||
$ws = $xmlDoc.CreateWhitespace("`r`n`t")
|
||||
$root.InsertBefore($eventsSection, $firstChild) | Out-Null
|
||||
$root.InsertBefore($ws, $eventsSection) | Out-Null
|
||||
} else {
|
||||
$root.AppendChild($xmlDoc.CreateWhitespace("`r`n`t")) | Out-Null
|
||||
$root.AppendChild($eventsSection) | Out-Null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$evtChildIndent = Get-ChildIndent $eventsSection
|
||||
if (-not $evtChildIndent -or $evtChildIndent -eq "") { $evtChildIndent = "`t`t" }
|
||||
|
||||
# Generate event fragments
|
||||
$script:xml = New-Object System.Text.StringBuilder 512
|
||||
X "<_F $allNsDecl>"
|
||||
foreach ($fe in $def.formEvents) {
|
||||
$feName = "$($fe.name)"
|
||||
$feHandler = "$($fe.handler)"
|
||||
$callTypeAttr = if ($fe.callType) { " callType=`"$($fe.callType)`"" } else { "" }
|
||||
X "$evtChildIndent<Event name=`"$feName`"$callTypeAttr>$feHandler</Event>"
|
||||
$ctStr = if ($fe.callType) { "[$($fe.callType)]" } else { "" }
|
||||
$addedFormEvents += " + $feName${ctStr} -> $feHandler"
|
||||
}
|
||||
X "</_F>"
|
||||
|
||||
$fragDoc = Parse-Fragment $script:xml.ToString()
|
||||
$importedEvents = Import-ElementNodes $fragDoc
|
||||
|
||||
foreach ($node in $importedEvents) {
|
||||
Insert-IntoContainer -container $eventsSection -newNode $node -afterName $null -childIndent $evtChildIndent
|
||||
}
|
||||
}
|
||||
|
||||
# === 12c. Add element-level events ===
|
||||
|
||||
$addedElemEvents = @()
|
||||
|
||||
if ($def.elementEvents -and $def.elementEvents.Count -gt 0) {
|
||||
if (-not $rootCI) {
|
||||
$rootCI = $root.SelectSingleNode("f:ChildItems", $nsMgr)
|
||||
}
|
||||
|
||||
foreach ($ee in $def.elementEvents) {
|
||||
$targetName = "$($ee.element)"
|
||||
$targetEl = Find-Element $rootCI $targetName
|
||||
if (-not $targetEl) {
|
||||
Write-Host "[WARN] Element '$targetName' not found — skipping elementEvent"
|
||||
continue
|
||||
}
|
||||
|
||||
# Find or create Events element within the target
|
||||
$targetEvents = $targetEl.SelectSingleNode("f:Events", $nsMgr)
|
||||
if (-not $targetEvents) {
|
||||
$targetEvents = $xmlDoc.CreateElement("Events", $formNs)
|
||||
# Insert Events before closing tag (after last property, before ChildItems if any)
|
||||
$ciNode = $targetEl.SelectSingleNode("f:ChildItems", $nsMgr)
|
||||
if ($ciNode) {
|
||||
$ws = $xmlDoc.CreateWhitespace("`r`n" + (Get-ChildIndent $targetEl))
|
||||
$targetEl.InsertBefore($ws, $ciNode) | Out-Null
|
||||
$targetEl.InsertBefore($targetEvents, $ciNode) | Out-Null
|
||||
} else {
|
||||
$trailing = $targetEl.LastChild
|
||||
if ($trailing -and ($trailing.NodeType -eq 'Whitespace' -or $trailing.NodeType -eq 'SignificantWhitespace')) {
|
||||
$ws = $xmlDoc.CreateWhitespace("`r`n" + (Get-ChildIndent $targetEl))
|
||||
$targetEl.InsertBefore($ws, $trailing) | Out-Null
|
||||
$targetEl.InsertBefore($targetEvents, $trailing) | Out-Null
|
||||
} else {
|
||||
$targetEl.AppendChild($xmlDoc.CreateWhitespace("`r`n" + (Get-ChildIndent $targetEl))) | Out-Null
|
||||
$targetEl.AppendChild($targetEvents) | Out-Null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$eeChildIndent = Get-ChildIndent $targetEvents
|
||||
if (-not $eeChildIndent -or $eeChildIndent -eq "") {
|
||||
$parentIndent = Get-ChildIndent $targetEl
|
||||
$eeChildIndent = "$parentIndent`t"
|
||||
}
|
||||
|
||||
# Create Event element
|
||||
$eeName = "$($ee.name)"
|
||||
$eeHandler = "$($ee.handler)"
|
||||
$callTypeAttr = if ($ee.callType) { " callType=`"$($ee.callType)`"" } else { "" }
|
||||
|
||||
$script:xml = New-Object System.Text.StringBuilder 256
|
||||
X "<_F $allNsDecl>"
|
||||
X "$eeChildIndent<Event name=`"$eeName`"$callTypeAttr>$eeHandler</Event>"
|
||||
X "</_F>"
|
||||
|
||||
$fragDoc = Parse-Fragment $script:xml.ToString()
|
||||
$importedEE = Import-ElementNodes $fragDoc
|
||||
|
||||
foreach ($node in $importedEE) {
|
||||
Insert-IntoContainer -container $targetEvents -newNode $node -afterName $null -childIndent $eeChildIndent
|
||||
}
|
||||
|
||||
$ctStr = if ($ee.callType) { "[$($ee.callType)]" } else { "" }
|
||||
$addedElemEvents += " + $targetName.$eeName${ctStr} -> $eeHandler"
|
||||
}
|
||||
}
|
||||
|
||||
# === 13. Save ===
|
||||
|
||||
$content = $xmlDoc.OuterXml
|
||||
@@ -999,6 +1154,23 @@ $enc = New-Object System.Text.UTF8Encoding($true)
|
||||
|
||||
# === 14. Summary ===
|
||||
|
||||
if ($script:isExtension) {
|
||||
Write-Host "[EXTENSION] BaseForm detected — IDs start at 1000000+"
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
if ($addedFormEvents.Count -gt 0) {
|
||||
Write-Host "Added form events:"
|
||||
foreach ($line in $addedFormEvents) { Write-Host $line }
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
if ($addedElemEvents.Count -gt 0) {
|
||||
Write-Host "Added element events:"
|
||||
foreach ($line in $addedElemEvents) { Write-Host $line }
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
if ($addedElems.Count -gt 0) {
|
||||
$posStr = ""
|
||||
if ($def.into) { $posStr += "into $($def.into)" }
|
||||
@@ -1023,6 +1195,8 @@ if ($addedCmds.Count -gt 0) {
|
||||
|
||||
Write-Host "---"
|
||||
$totalParts = @()
|
||||
if ($addedFormEvents.Count -gt 0) { $totalParts += "$($addedFormEvents.Count) form event(s)" }
|
||||
if ($addedElemEvents.Count -gt 0) { $totalParts += "$($addedElemEvents.Count) element event(s)" }
|
||||
if ($addedElems.Count -gt 0) {
|
||||
$compStr = if ($companionCount -gt 0) { " (+$companionCount companions)" } else { "" }
|
||||
$totalParts += "$($addedElems.Count) element(s)$compStr"
|
||||
|
||||
@@ -45,6 +45,11 @@ powershell.exe -NoProfile -File .claude/skills/form-info/scripts/form-info.ps1 -
|
||||
=== Form: ФормаДокумента — "Реализация товаров и услуг" (Documents.РеализацияТоваровУслуг) ===
|
||||
```
|
||||
|
||||
Для заимствованных форм расширения (с `<BaseForm>`):
|
||||
```
|
||||
=== Form: ФормаЭлемента [EXTENSION] (Catalogs.Валюты) ===
|
||||
```
|
||||
|
||||
Имя формы, заголовок (Title) и контекст объекта определяются из пути к файлу и XML.
|
||||
|
||||
### Properties — свойства формы
|
||||
@@ -63,6 +68,13 @@ Events:
|
||||
OnOpen -> ПриОткрытии
|
||||
```
|
||||
|
||||
Для расширений с callType:
|
||||
```
|
||||
Events:
|
||||
OnCreateAtServer[After] -> Расш1_ПриСозданииПосле
|
||||
OnOpen[Before] -> Расш1_ПриОткрытии
|
||||
```
|
||||
|
||||
### Elements — дерево UI-элементов
|
||||
|
||||
Компактное дерево с типами, привязками к данным, флагами и событиями:
|
||||
@@ -114,7 +126,7 @@ Elements:
|
||||
|
||||
**Привязка к команде**: `-> ИмяКоманды [cmd]` — команда формы, `-> Close [std]` — стандартная команда
|
||||
|
||||
**События**: `{OnChange, StartChoice}` — имена обработчиков
|
||||
**События**: `{OnChange, StartChoice}` — имена обработчиков; `{OnChange[Before]}` — с callType для расширений
|
||||
|
||||
**Заголовок**: `[title:Текст]` — только если отличается от имени элемента
|
||||
|
||||
@@ -151,8 +163,21 @@ Commands:
|
||||
Заполнить -> ЗаполнитьОбработка
|
||||
```
|
||||
|
||||
Для расширений с callType на Action:
|
||||
```
|
||||
Commands:
|
||||
Подбор -> Расш1_ПодборПеред[Before], Расш1_ПодборПосле[After]
|
||||
```
|
||||
|
||||
Формат: `Имя -> Обработчик [Сочетание]`
|
||||
|
||||
### BaseForm (расширения)
|
||||
|
||||
Для заимствованных форм в конце выводится:
|
||||
```
|
||||
BaseForm: present (version 2.17)
|
||||
```
|
||||
|
||||
## Что пропускается
|
||||
|
||||
Скрипт убирает 80%+ XML-объёма:
|
||||
|
||||
@@ -35,6 +35,10 @@ $ns.AddNamespace("dcsset", "http://v8.1c.ru/8.1/data-composition-system/settings
|
||||
|
||||
$root = $xmlDoc.DocumentElement
|
||||
|
||||
# --- Detect extension (BaseForm) ---
|
||||
$baseFormNode = $root.SelectSingleNode("d:BaseForm", $ns)
|
||||
$isExtension = ($baseFormNode -ne $null)
|
||||
|
||||
# --- Helper: extract multilang text ---
|
||||
|
||||
function Get-MLText($node) {
|
||||
@@ -140,7 +144,10 @@ function Get-EventsStr($node) {
|
||||
if (-not $eventsNode) { return "" }
|
||||
$evts = @()
|
||||
foreach ($e in $eventsNode.SelectNodes("d:Event", $ns)) {
|
||||
$evts += $e.GetAttribute("name")
|
||||
$eName = $e.GetAttribute("name")
|
||||
$ct = $e.GetAttribute("callType")
|
||||
if ($ct) { $evts += "$eName[$ct]" }
|
||||
else { $evts += $eName }
|
||||
}
|
||||
if ($evts.Count -eq 0) { return "" }
|
||||
return " {$($evts -join ', ')}"
|
||||
@@ -340,7 +347,8 @@ if ($titleNode) {
|
||||
$formTitle = Get-MLText $titleNode
|
||||
if (-not $formTitle) { $formTitle = $titleNode.InnerText }
|
||||
}
|
||||
$header = "=== Form: $formName"
|
||||
$extMarker = if ($isExtension) { " [EXTENSION]" } else { "" }
|
||||
$header = "=== Form: $formName$extMarker"
|
||||
if ($formTitle) { $header += " — `"$formTitle`"" }
|
||||
if ($objectContext) { $header += " ($objectContext)" }
|
||||
$header += " ==="
|
||||
@@ -390,7 +398,9 @@ if ($formEvents -and $formEvents.HasChildNodes) {
|
||||
foreach ($e in $formEvents.SelectNodes("d:Event", $ns)) {
|
||||
$eName = $e.GetAttribute("name")
|
||||
$eHandler = $e.InnerText
|
||||
$lines += " $eName -> $eHandler"
|
||||
$ct = $e.GetAttribute("callType")
|
||||
$ctStr = if ($ct) { "[$ct]" } else { "" }
|
||||
$lines += " $eName${ctStr} -> $eHandler"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -491,12 +501,27 @@ 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 { "" }
|
||||
|
||||
# Collect all Action elements (may have multiple with callType)
|
||||
$actions = $cmd.SelectNodes("d:Action", $ns)
|
||||
if ($actions.Count -gt 1) {
|
||||
$actParts = @()
|
||||
foreach ($a in $actions) {
|
||||
$ct = $a.GetAttribute("callType")
|
||||
$ctStr = if ($ct) { "[$ct]" } else { "" }
|
||||
$actParts += "$($a.InnerText)$ctStr"
|
||||
}
|
||||
$actionStr = " -> $($actParts -join ', ')"
|
||||
} elseif ($actions.Count -eq 1) {
|
||||
$ct = $actions[0].GetAttribute("callType")
|
||||
$ctStr = if ($ct) { "[$ct]" } else { "" }
|
||||
$actionStr = " -> $($actions[0].InnerText)$ctStr"
|
||||
} else {
|
||||
$actionStr = ""
|
||||
}
|
||||
|
||||
$cmdLines += " $cName$actionStr$scStr"
|
||||
}
|
||||
if ($cmdLines.Count -gt 0) {
|
||||
@@ -506,6 +531,15 @@ if ($cmdsNode) {
|
||||
}
|
||||
}
|
||||
|
||||
# --- BaseForm footer ---
|
||||
|
||||
if ($isExtension) {
|
||||
$bfVersion = $baseFormNode.GetAttribute("version")
|
||||
$bfStr = if ($bfVersion) { "present (version $bfVersion)" } else { "present" }
|
||||
$lines += ""
|
||||
$lines += "BaseForm: $bfStr"
|
||||
}
|
||||
|
||||
# --- Truncation protection ---
|
||||
|
||||
$totalLines = $lines.Count
|
||||
|
||||
@@ -46,6 +46,10 @@ powershell.exe -NoProfile -File .claude/skills/form-validate/scripts/form-valida
|
||||
| 9 | События имеют непустые имена обработчиков | ERROR |
|
||||
| 10 | Команды имеют Action (обработчик) | ERROR |
|
||||
| 11 | Не более одного MainAttribute | ERROR |
|
||||
| 12 | BaseForm: наличие и version (при расширении) | OK / WARN |
|
||||
| 13 | callType значения: Before, After, Override | ERROR |
|
||||
| 14 | ID расширения >= 1000000 для добавленных attrs/commands | WARN |
|
||||
| 15 | callType без BaseForm — некорректная структура | WARN |
|
||||
|
||||
## Вывод
|
||||
|
||||
@@ -71,8 +75,18 @@ All checks passed.
|
||||
|
||||
Код возврата: 0 = все проверки пройдены, 1 = есть ошибки.
|
||||
|
||||
### Расширения
|
||||
|
||||
При обнаружении `<BaseForm>` автоматически активируются дополнительные проверки:
|
||||
- Валидность значений `callType` (Before/After/Override)
|
||||
- ID расширения >= 1000000 для добавленных атрибутов и команд
|
||||
- Наличие version на `<BaseForm>`
|
||||
|
||||
Формы без `<BaseForm>` проверяются только стандартными проверками.
|
||||
|
||||
## Когда использовать
|
||||
|
||||
- **После `/form-compile`**: проверить корректность сгенерированной формы
|
||||
- **После `/form-edit`**: проверить добавленные элементы, особенно в extension-формах
|
||||
- **После ручного редактирования Form.xml**: убедиться что ID уникальны, companions на месте, ссылки валидны
|
||||
- **При отладке**: выявить ошибки в структуре формы до сборки EPF
|
||||
|
||||
@@ -479,6 +479,152 @@ if (-not $stopped) {
|
||||
}
|
||||
}
|
||||
|
||||
# --- Check 11: Extension-specific validations ---
|
||||
|
||||
$baseFormNode = $root.SelectSingleNode("f:BaseForm", $nsMgr)
|
||||
$isExtension = ($baseFormNode -ne $null)
|
||||
|
||||
if (-not $stopped -and $isExtension) {
|
||||
# 11a. BaseForm version
|
||||
$bfVersion = $baseFormNode.GetAttribute("version")
|
||||
if ($bfVersion) {
|
||||
Report-OK "BaseForm: version=$bfVersion"
|
||||
} else {
|
||||
Report-Warn "BaseForm: version attribute missing"
|
||||
}
|
||||
|
||||
# 11b. callType values validation (Before, After, Override)
|
||||
$validCallTypes = @("Before", "After", "Override")
|
||||
$ctErrors = 0
|
||||
$ctChecked = 0
|
||||
|
||||
# Check form-level events
|
||||
$formEventsNode = $root.SelectSingleNode("f:Events", $nsMgr)
|
||||
if ($formEventsNode) {
|
||||
foreach ($evt in $formEventsNode.SelectNodes("f:Event", $nsMgr)) {
|
||||
$ct = $evt.GetAttribute("callType")
|
||||
if ($ct) {
|
||||
$ctChecked++
|
||||
if ($validCallTypes -notcontains $ct) {
|
||||
Report-Error "Form event '$($evt.GetAttribute('name'))': invalid callType='$ct' (expected: Before, After, Override)"
|
||||
$ctErrors++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Check element-level events
|
||||
foreach ($el in $allElements) {
|
||||
if ($stopped) { break }
|
||||
$eventsNode = $el.Node.SelectSingleNode("f:Events", $nsMgr)
|
||||
if (-not $eventsNode) { continue }
|
||||
foreach ($evt in $eventsNode.SelectNodes("f:Event", $nsMgr)) {
|
||||
$ct = $evt.GetAttribute("callType")
|
||||
if ($ct) {
|
||||
$ctChecked++
|
||||
if ($validCallTypes -notcontains $ct) {
|
||||
Report-Error "[$($el.Tag)] '$($el.Name)' event '$($evt.GetAttribute('name'))': invalid callType='$ct'"
|
||||
$ctErrors++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Check command actions
|
||||
foreach ($cmd in $cmdNodes) {
|
||||
if ($stopped) { break }
|
||||
$cmdName = $cmd.GetAttribute("name")
|
||||
foreach ($action in $cmd.SelectNodes("f:Action", $nsMgr)) {
|
||||
$ct = $action.GetAttribute("callType")
|
||||
if ($ct) {
|
||||
$ctChecked++
|
||||
if ($validCallTypes -notcontains $ct) {
|
||||
Report-Error "Command '$cmdName' Action: invalid callType='$ct'"
|
||||
$ctErrors++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $stopped -and $ctErrors -eq 0 -and $ctChecked -gt 0) {
|
||||
Report-OK "callType values: $ctChecked checked"
|
||||
}
|
||||
|
||||
# 11c. Extension ID ranges — warn if extension-added attrs/commands have id < 1000000
|
||||
# Collect BaseForm attribute names to distinguish added ones
|
||||
$baseAttrNames = @{}
|
||||
$baseCmdNames = @{}
|
||||
$bfNs = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
|
||||
$bfNs.AddNamespace("f", "http://v8.1c.ru/8.3/xcf/logform")
|
||||
foreach ($bAttr in $baseFormNode.SelectNodes("f:Attributes/f:Attribute", $bfNs)) {
|
||||
$baName = $bAttr.GetAttribute("name")
|
||||
if ($baName) { $baseAttrNames[$baName] = $true }
|
||||
}
|
||||
foreach ($bCmd in $baseFormNode.SelectNodes("f:Commands/f:Command", $bfNs)) {
|
||||
$bcName = $bCmd.GetAttribute("name")
|
||||
if ($bcName) { $baseCmdNames[$bcName] = $true }
|
||||
}
|
||||
|
||||
$idWarnCount = 0
|
||||
foreach ($attr in $attrNodes) {
|
||||
$aName = $attr.GetAttribute("name")
|
||||
$aId = $attr.GetAttribute("id")
|
||||
if ($aName -and -not $baseAttrNames.ContainsKey($aName) -and $aId) {
|
||||
try {
|
||||
$intId = [int]$aId
|
||||
if ($intId -lt 1000000) {
|
||||
Report-Warn "Attribute '$aName' (id=$aId): extension-added attribute has id < 1000000"
|
||||
$idWarnCount++
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($cmd in $cmdNodes) {
|
||||
$cName = $cmd.GetAttribute("name")
|
||||
$cId = $cmd.GetAttribute("id")
|
||||
if ($cName -and -not $baseCmdNames.ContainsKey($cName) -and $cId) {
|
||||
try {
|
||||
$intId = [int]$cId
|
||||
if ($intId -lt 1000000) {
|
||||
Report-Warn "Command '$cName' (id=$cId): extension-added command has id < 1000000"
|
||||
$idWarnCount++
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $stopped -and $idWarnCount -eq 0) {
|
||||
$extAttrCount = ($attrNodes | Where-Object { -not $baseAttrNames.ContainsKey($_.GetAttribute("name")) }).Count
|
||||
$extCmdCount = ($cmdNodes | Where-Object { -not $baseCmdNames.ContainsKey($_.GetAttribute("name")) }).Count
|
||||
if (($extAttrCount + $extCmdCount) -gt 0) {
|
||||
Report-OK "Extension ID ranges: $extAttrCount attr(s), $extCmdCount cmd(s) — all >= 1000000"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Check callType without BaseForm (structural warning)
|
||||
if (-not $stopped -and -not $isExtension) {
|
||||
$callTypeWithoutBase = $false
|
||||
$feNode = $root.SelectSingleNode("f:Events", $nsMgr)
|
||||
if ($feNode) {
|
||||
foreach ($evt in $feNode.SelectNodes("f:Event", $nsMgr)) {
|
||||
if ($evt.GetAttribute("callType")) { $callTypeWithoutBase = $true; break }
|
||||
}
|
||||
}
|
||||
if (-not $callTypeWithoutBase) {
|
||||
foreach ($cmd in $cmdNodes) {
|
||||
foreach ($action in $cmd.SelectNodes("f:Action", $nsMgr)) {
|
||||
if ($action.GetAttribute("callType")) { $callTypeWithoutBase = $true; break }
|
||||
}
|
||||
if ($callTypeWithoutBase) { break }
|
||||
}
|
||||
}
|
||||
if ($callTypeWithoutBase) {
|
||||
Report-Warn "callType attributes found but no BaseForm — possible incorrect structure"
|
||||
}
|
||||
}
|
||||
|
||||
# --- Summary ---
|
||||
|
||||
Write-Host ""
|
||||
|
||||
Reference in New Issue
Block a user