diff --git a/.claude/skills/form-compile/scripts/form-compile.ps1 b/.claude/skills/form-compile/scripts/form-compile.ps1 index 2feb6f61..7444ebc0 100644 --- a/.claude/skills/form-compile/scripts/form-compile.ps1 +++ b/.claude/skills/form-compile/scripts/form-compile.ps1 @@ -1,4 +1,4 @@ -# form-compile v1.67 — Compile 1C managed form from JSON or object metadata +# form-compile v1.68 — Compile 1C managed form from JSON or object metadata # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [string]$JsonPath, @@ -2940,6 +2940,11 @@ function Emit-Input { Emit-ChoiceList -el $el -indent $inner + # Связи по типу / связи параметров выбора / параметры выбора + Emit-TypeLink -el $el -indent $inner + Emit-ChoiceParameterLinks -el $el -indent $inner + Emit-ChoiceParameters -el $el -indent $inner + # Оформление (цвета/шрифты/граница) — перед компаньонами Emit-Appearance -el $el -indent $inner -profile 'field' @@ -3177,6 +3182,103 @@ function Emit-ChoiceList { X "$indent" } +# Чтение свойства из hashtable/PSCustomObject по списку синонимов (первый найденный, иначе $null). +function Get-ElProp { + param($obj, [string[]]$names) + if ($null -eq $obj) { return $null } + foreach ($n in $names) { + if ($obj -is [System.Collections.IDictionary]) { + if ($obj.Contains($n)) { return $obj[$n] } + } elseif ($obj.PSObject -and $obj.PSObject.Properties[$n]) { + return $obj.PSObject.Properties[$n].Value + } + } + return $null +} + +# Внутреннее значение параметра выбора (FormChoiceListDesTimeValue): + . +# Скаляр → один Value (через Normalize-ChoiceValue); массив → v8:FixedArray из вложенных FormChoiceListDesTimeValue. +function Emit-ChoiceParamValue { + param($value, [string]$indent) + X "$indent" + $isArray = ($value -is [System.Array]) -or ($value -is [System.Collections.IList] -and $value -isnot [string]) + if ($isArray) { + X "$indent" + foreach ($v in $value) { + $norm = Normalize-ChoiceValue -value $v + X "$indent`t" + X "$indent`t`t" + X "$indent`t`t$(Esc-Xml $norm.Text)" + X "$indent`t" + } + X "$indent" + } else { + $norm = Normalize-ChoiceValue -value $value + X "$indent$(Esc-Xml $norm.Text)" + } +} + +# (параметры выбора поля ввода) — [{name, value}]. value через Normalize-ChoiceValue; +# массив значений → FixedArray. Рус. синонимы имя/значение. +function Emit-ChoiceParameters { + param($el, [string]$indent) + $cp = $el.choiceParameters + if (-not $cp -or @($cp).Count -eq 0) { return } + X "$indent" + foreach ($item in @($cp)) { + $name = Get-ElProp $item @('name','имя') + $val = Get-ElProp $item @('value','значение') + X "$indent`t" + X "$indent`t`t" + Emit-ChoiceParamValue -value $val -indent "$indent`t`t`t" + X "$indent`t`t" + X "$indent`t" + } + X "$indent" +} + +# (связи параметров выбора) — [{name, dataPath, valueChange?}]. +# valueChange всегда эмитится, дефолт Clear; forgiving Clear/DontChange + рус. синонимы. +function Emit-ChoiceParameterLinks { + param($el, [string]$indent) + $cpl = $el.choiceParameterLinks + if (-not $cpl -or @($cpl).Count -eq 0) { return } + X "$indent" + foreach ($lk in @($cpl)) { + $name = Get-ElProp $lk @('name','имя') + $dp = Get-ElProp $lk @('dataPath','path','путь') + $vcRaw = Get-ElProp $lk @('valueChange','режимИзменения') + $vc = "Clear" + if ($vcRaw) { + $vc = switch -Regex ("$vcRaw".ToLower()) { + '^(clear|очистить|очистка)$' { "Clear"; break } + '^(dontchange|неизменять|неменять|нет)$' { "DontChange"; break } + default { "$vcRaw" } + } + } + X "$indent`t" + X "$indent`t`t$(Esc-Xml "$name")" + X "$indent`t`t$(Esc-Xml "$dp")" + X "$indent`t`t$vc" + X "$indent`t" + } + X "$indent" +} + +# (связь по типу) — {dataPath, linkItem}. linkItem дефолт 0. +function Emit-TypeLink { + param($el, [string]$indent) + $tl = $el.typeLink + if (-not $tl) { return } + $dp = Get-ElProp $tl @('dataPath','path','путь') + $li = Get-ElProp $tl @('linkItem','элементСвязи') + if ($null -eq $li) { $li = 0 } + X "$indent" + X "$indent`t$(Esc-Xml "$dp")" + X "$indent`t$li" + X "$indent" +} + function Emit-Radio { param($el, [string]$name, [int]$id, [string]$indent) diff --git a/.claude/skills/form-compile/scripts/form-compile.py b/.claude/skills/form-compile/scripts/form-compile.py index 304948b6..84c09b99 100644 --- a/.claude/skills/form-compile/scripts/form-compile.py +++ b/.claude/skills/form-compile/scripts/form-compile.py @@ -2001,6 +2001,99 @@ def emit_choice_list(lines, el, indent): lines.append(f'{indent}') +def get_el_prop(obj, names): + # Читает свойство из dict по списку синонимов (первый найденный, иначе None). + if not isinstance(obj, dict): + return None + for n in names: + if n in obj: + return obj[n] + return None + + +def emit_choice_param_value(lines, value, indent): + # Внутреннее значение параметра выбора (FormChoiceListDesTimeValue): + . + # Скаляр → один Value; массив → v8:FixedArray из вложенных FormChoiceListDesTimeValue. + lines.append(f'{indent}') + if isinstance(value, (list, tuple)): + lines.append(f'{indent}') + for v in value: + norm = normalize_choice_value(v) + lines.append(f'{indent}\t') + lines.append(f'{indent}\t\t') + lines.append(f'{indent}\t\t{esc_xml(norm["text"])}') + lines.append(f'{indent}\t') + lines.append(f'{indent}') + else: + norm = normalize_choice_value(value) + lines.append(f'{indent}{esc_xml(norm["text"])}') + + +def emit_choice_parameters(lines, el, indent): + # (параметры выбора поля ввода) — [{name, value}]. value через + # normalize_choice_value; массив значений → FixedArray. Рус. синонимы имя/значение. + cp = el.get('choiceParameters') or [] + if not cp: + return + lines.append(f'{indent}') + for item in cp: + name = get_el_prop(item, ('name', 'имя')) + val = get_el_prop(item, ('value', 'значение')) + name_s = '' if name is None else str(name) + lines.append(f'{indent}\t') + lines.append(f'{indent}\t\t') + emit_choice_param_value(lines, val, f'{indent}\t\t\t') + lines.append(f'{indent}\t\t') + lines.append(f'{indent}\t') + lines.append(f'{indent}') + + +def emit_choice_parameter_links(lines, el, indent): + # (связи параметров выбора) — [{name, dataPath, valueChange?}]. + # valueChange всегда эмитится, дефолт Clear; forgiving Clear/DontChange + рус. синонимы. + cpl = el.get('choiceParameterLinks') or [] + if not cpl: + return + lines.append(f'{indent}') + for lk in cpl: + name = get_el_prop(lk, ('name', 'имя')) + dp = get_el_prop(lk, ('dataPath', 'path', 'путь')) + vc_raw = get_el_prop(lk, ('valueChange', 'режимИзменения')) + vc = 'Clear' + if vc_raw: + s = str(vc_raw).lower() + if s in ('clear', 'очистить', 'очистка'): + vc = 'Clear' + elif s in ('dontchange', 'неизменять', 'неменять', 'нет'): + vc = 'DontChange' + else: + vc = str(vc_raw) + name_s = '' if name is None else str(name) + dp_s = '' if dp is None else str(dp) + lines.append(f'{indent}\t') + lines.append(f'{indent}\t\t{esc_xml(name_s)}') + lines.append(f'{indent}\t\t{esc_xml(dp_s)}') + lines.append(f'{indent}\t\t{vc}') + lines.append(f'{indent}\t') + lines.append(f'{indent}') + + +def emit_type_link(lines, el, indent): + # (связь по типу) — {dataPath, linkItem}. linkItem дефолт 0. + tl = el.get('typeLink') + if not tl: + return + dp = get_el_prop(tl, ('dataPath', 'path', 'путь')) + li = get_el_prop(tl, ('linkItem', 'элементСвязи')) + if li is None: + li = 0 + dp_s = '' if dp is None else str(dp) + lines.append(f'{indent}') + lines.append(f'{indent}\t{esc_xml(dp_s)}') + lines.append(f'{indent}\t{li}') + lines.append(f'{indent}') + + def normalize_radio_button_type(raw): if not raw: return "Auto" @@ -2847,6 +2940,11 @@ def emit_input(lines, el, name, eid, indent): emit_choice_list(lines, el, inner) + # Связи по типу / связи параметров выбора / параметры выбора + emit_type_link(lines, el, inner) + emit_choice_parameter_links(lines, el, inner) + emit_choice_parameters(lines, el, inner) + # Оформление (цвета/шрифты/граница) — перед компаньонами emit_appearance(lines, el, inner, 'field') diff --git a/.claude/skills/form-decompile/scripts/form-decompile.ps1 b/.claude/skills/form-decompile/scripts/form-decompile.ps1 index 7b8e4bcd..0ddd7585 100644 --- a/.claude/skills/form-decompile/scripts/form-decompile.ps1 +++ b/.claude/skills/form-decompile/scripts/form-decompile.ps1 @@ -1,4 +1,4 @@ -# form-decompile v0.47 — Decompile 1C managed Form.xml to JSON DSL (draft) +# form-decompile v0.48 — Decompile 1C managed Form.xml to JSON DSL (draft) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # ВНИМАНИЕ: раундтрип не гарантируется. Навык исключён из авто-использования моделью. param( @@ -40,6 +40,7 @@ $NS_DCSSET = "http://v8.1c.ru/8.1/data-composition-system/settings" $NS_DCSSCH = "http://v8.1c.ru/8.1/data-composition-system/schema" $NS_DCSCOR = "http://v8.1c.ru/8.1/data-composition-system/core" $NS_V8UI = "http://v8.1c.ru/8.1/data/ui" +$NS_APP = "http://v8.1c.ru/8.2/managed-application/core" $ns = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable) $ns.AddNamespace("lf", $NS_LF) @@ -50,6 +51,7 @@ $ns.AddNamespace("dcsset", $NS_DCSSET) $ns.AddNamespace("dcssch", $NS_DCSSCH) $ns.AddNamespace("dcscor", $NS_DCSCOR) $ns.AddNamespace("v8ui", $NS_V8UI) +$ns.AddNamespace("app", $NS_APP) # Каноничные GUID пустых контейнеров ListSettings (умолчание платформы, ~90% форм). # Если ListSettings = пустой скелет с этими GUID → декомпилятор опускает настройки вовсе, @@ -1178,6 +1180,70 @@ function Decompile-ChoiceList { return $null } +# Значение Параметра выбора (): скаляр через Convert-TypedValue, либо +# v8:FixedArray → массив скаляров (каждый — FormChoiceListDesTimeValue с внутренним lf:Value). +function Convert-ChoiceParamValue { + param($valNode) + $vt = $valNode.GetAttribute("type", $NS_XSI) + if ($vt -match 'FixedArray$') { + $arr = New-Object System.Collections.ArrayList + foreach ($it in @($valNode.SelectNodes("v8:Value", $ns))) { + $inner = $it.SelectSingleNode("lf:Value", $ns) + if ($inner) { [void]$arr.Add((Convert-TypedValue -raw $inner.InnerText -xsiType ($inner.GetAttribute("type", $NS_XSI)))) } + } + return ,@($arr) + } + return Convert-TypedValue -raw $valNode.InnerText -xsiType $vt +} + +# Инверсия Emit-ChoiceParameters: → [{name, value}]. +function Decompile-ChoiceParameters { + param($node) + $cpn = $node.SelectSingleNode("lf:ChoiceParameters", $ns) + if (-not $cpn) { return $null } + $items = New-Object System.Collections.ArrayList + foreach ($it in @($cpn.SelectNodes("app:item", $ns))) { + $o = [ordered]@{} + $o['name'] = $it.GetAttribute("name") + $valNode = $it.SelectSingleNode("app:value/lf:Value", $ns) + if ($valNode) { $o['value'] = Convert-ChoiceParamValue $valNode } + [void]$items.Add($o) + } + if ($items.Count -gt 0) { return ,@($items) } + return $null +} + +# Инверсия Emit-ChoiceParameterLinks: → +# [{name, dataPath, valueChange?}]. valueChange дефолт Clear → опускаем (компилятор восстановит). +function Decompile-ChoiceParameterLinks { + param($node) + $cln = $node.SelectSingleNode("lf:ChoiceParameterLinks", $ns) + if (-not $cln) { return $null } + $items = New-Object System.Collections.ArrayList + foreach ($lk in @($cln.SelectNodes("xr:Link", $ns))) { + $o = [ordered]@{} + $o['name'] = Get-Text $lk "xr:Name" + $o['dataPath'] = Get-Text $lk "xr:DataPath" + $vc = Get-Text $lk "xr:ValueChange" + if ($vc -and $vc -ne 'Clear') { $o['valueChange'] = $vc } + [void]$items.Add($o) + } + if ($items.Count -gt 0) { return ,@($items) } + return $null +} + +# Инверсия Emit-TypeLink: → {dataPath, linkItem}. +function Decompile-TypeLink { + param($node) + $tn = $node.SelectSingleNode("lf:TypeLink", $ns) + if (-not $tn) { return $null } + $o = [ordered]@{} + $o['dataPath'] = Get-Text $tn "xr:DataPath" + $li = Get-Text $tn "xr:LinkItem" + if ($null -ne $li -and $li -ne '') { $o['linkItem'] = [int]$li } + return $o +} + function Decompile-Element { param($node) $tag = $node.LocalName @@ -1244,6 +1310,10 @@ function Decompile-Element { } $cbr = Get-Child $node 'ChoiceButtonRepresentation'; if ($cbr) { $obj['choiceButtonRepresentation'] = $cbr } $cl = Decompile-ChoiceList $node; if ($cl) { $obj['choiceList'] = $cl } + # Параметры выбора / Связи параметров выбора / Связь по типу + $cp = Decompile-ChoiceParameters $node; if ($cp) { $obj['choiceParameters'] = $cp } + $cpl = Decompile-ChoiceParameterLinks $node; if ($cpl) { $obj['choiceParameterLinks'] = $cpl } + $tlk = Decompile-TypeLink $node; if ($tlk) { $obj['typeLink'] = $tlk } } 'CheckBoxField' { $obj[$key] = $name diff --git a/docs/form-dsl-spec.md b/docs/form-dsl-spec.md index 1cbf9abf..efc0449e 100644 --- a/docs/form-dsl-spec.md +++ b/docs/form-dsl-spec.md @@ -364,6 +364,31 @@ companion-панели с собственным контентом. Оба не | `verticalStretch` | bool | Растягивание по вертикали | | `autoMaxWidth` | bool | Автомаксимальная ширина | | `autoMaxHeight` | bool | Автомаксимальная высота | +| `choiceParameters` | array | Параметры выбора — см. ниже | +| `choiceParameterLinks` | array | Связи параметров выбора — см. ниже | +| `typeLink` | object | Связь по типу — см. ниже | + +##### Параметры выбора, связи параметров выбора, связь по типу + +Свойства поля ввода, управляющие выбором значения и типом. Имена параметров (`"Отбор.Х"`) — строки 1С как есть. + +```json +{ "input": "Контрагент", "path": "Объект.Контрагент", + "choiceParameters": [ + { "name": "Отбор.Активный", "value": true }, + { "name": "Отбор.ВидПродукции", "value": ["Enum.Виды.Агрохимикат", "Enum.Виды.Пестицид"] } + ], + "choiceParameterLinks": [ + { "name": "Отбор.Организация", "dataPath": "Объект.Организация" }, + { "name": "Отбор.Тип", "dataPath": "Объект.Тип", "valueChange": "DontChange" } + ], + "typeLink": { "dataPath": "Объект.ЗначениеДата", "linkItem": 0 } +} +``` + +- **`choiceParameters`** — `[{ name, value }]`. `value` через ту же нормализацию, что `choiceList`: bool / число / строка / ссылка-путь (`Enum.X.Y`, `Catalog.X` и синонимы `Перечисление.`/`Справочник.`). **Массив** значений → фиксированный массив (`FixedArray`). Синонимы ключей: `имя`/`значение`. +- **`choiceParameterLinks`** — `[{ name, dataPath, valueChange? }]`. `valueChange`: `Clear` (дефолт, опускается) / `DontChange`; forgiving `очистить`/`неизменять`. Синонимы: `имя`/`путь`/`режимИзменения`. +- **`typeLink`** — `{ dataPath, linkItem }`. `linkItem` — индекс (дефолт `0`). Синонимы: `путь`/`элементСвязи`. #### check — CheckBoxField diff --git a/tests/skills/cases/form-compile/input-fields.json b/tests/skills/cases/form-compile/input-fields.json index 714591a4..e1129dcf 100644 --- a/tests/skills/cases/form-compile/input-fields.json +++ b/tests/skills/cases/form-compile/input-fields.json @@ -26,6 +26,18 @@ { "value": "Второй", "presentation": "Второй вариант" } ]}, { "input": "ПолеПодсказка", "path": "ПолеПодсказка", "inputHint": "Введите значение...", "title": "Подсказка" }, + { "input": "ПолеСвязи", "path": "ПолеСвязи", "title": "Поле со связями", + "choiceParameters": [ + { "name": "Отбор.Активный", "value": true }, + { "name": "Отбор.Вид", "value": "Основной" }, + { "name": "Отбор.Список", "value": ["Один", "Два"] } + ], + "choiceParameterLinks": [ + { "name": "Отбор.Организация", "dataPath": "ОбычноеПоле" }, + { "name": "Отбор.Тип", "dataPath": "ПолеСписокВыбора", "valueChange": "DontChange" } + ], + "typeLink": { "dataPath": "ПолеПодсказка", "linkItem": 0 } + }, { "check": "Флаг", "path": "Флаг", "title": "Включено" }, { "check": "ФлагПлатформенный", "path": "ФлагПлатформенный", "title": "Слева", "titleLocation": "" }, { "check": "ФлагЯвный", "path": "ФлагЯвный", "title": "Сверху", "titleLocation": "top" }, @@ -39,6 +51,7 @@ { "name": "ПолеСКнопками", "type": "string" }, { "name": "ПолеСписокВыбора", "type": "string" }, { "name": "ПолеПодсказка", "type": "string" }, + { "name": "ПолеСвязи", "type": "string" }, { "name": "Флаг", "type": "boolean" }, { "name": "ФлагПлатформенный", "type": "boolean" }, { "name": "ФлагЯвный", "type": "boolean" }, diff --git a/tests/skills/cases/form-compile/snapshots/input-fields/DataProcessors/ПоляВвода/Forms/Форма/Ext/Form.xml b/tests/skills/cases/form-compile/snapshots/input-fields/DataProcessors/ПоляВвода/Forms/Форма/Ext/Form.xml index 1e20b1d9..4a3887c2 100644 --- a/tests/skills/cases/form-compile/snapshots/input-fields/DataProcessors/ПоляВвода/Forms/Форма/Ext/Form.xml +++ b/tests/skills/cases/form-compile/snapshots/input-fields/DataProcessors/ПоляВвода/Forms/Форма/Ext/Form.xml @@ -138,7 +138,63 @@ - + + ПолеСвязи + + <v8:item> + <v8:lang>ru</v8:lang> + <v8:content>Поле со связями</v8:content> + </v8:item> + + + ПолеПодсказка + 0 + + + + Отбор.Организация + ОбычноеПоле + Clear + + + Отбор.Тип + ПолеСписокВыбора + DontChange + + + + + + + true + + + + + + Основной + + + + + + + + + Один + + + + Два + + + + + + + + + Флаг <v8:item> @@ -148,10 +204,10 @@ Auto Right - - + + - + ФлагПлатформенный <v8:item> @@ -160,10 +216,10 @@ </v8:item> Auto - - + + - + ФлагЯвный <v8:item> @@ -173,10 +229,10 @@ Auto Top - - + + - + ФлагТумблер <v8:item> @@ -186,18 +242,18 @@ Switcher Right - - + + - + cfg:DataProcessorObject.ПоляВвода true - + <v8:item> <v8:lang>ru</v8:lang> @@ -212,7 +268,7 @@ </v8:StringQualifiers> </Type> </Attribute> - <Attribute name="МногострочноеПоле" id="36"> + <Attribute name="МногострочноеПоле" id="39"> <Title> <v8:item> <v8:lang>ru</v8:lang> @@ -227,7 +283,7 @@ </v8:StringQualifiers> </Type> </Attribute> - <Attribute name="ПолеПароля" id="37"> + <Attribute name="ПолеПароля" id="40"> <Title> <v8:item> <v8:lang>ru</v8:lang> @@ -242,7 +298,7 @@ </v8:StringQualifiers> </Type> </Attribute> - <Attribute name="ПолеСКнопками" id="38"> + <Attribute name="ПолеСКнопками" id="41"> <Title> <v8:item> <v8:lang>ru</v8:lang> @@ -257,7 +313,7 @@ </v8:StringQualifiers> </Type> </Attribute> - <Attribute name="ПолеСписокВыбора" id="39"> + <Attribute name="ПолеСписокВыбора" id="42"> <Title> <v8:item> <v8:lang>ru</v8:lang> @@ -272,7 +328,7 @@ </v8:StringQualifiers> </Type> </Attribute> - <Attribute name="ПолеПодсказка" id="40"> + <Attribute name="ПолеПодсказка" id="43"> <Title> <v8:item> <v8:lang>ru</v8:lang> @@ -287,7 +343,22 @@ </v8:StringQualifiers> </Type> </Attribute> - <Attribute name="Флаг" id="41"> + <Attribute name="ПолеСвязи" id="44"> + <Title> + <v8:item> + <v8:lang>ru</v8:lang> + <v8:content>Поле связи</v8:content> + </v8:item> + + + xs:string + + 0 + Variable + + + + <v8:item> <v8:lang>ru</v8:lang> @@ -298,7 +369,7 @@ <v8:Type>xs:boolean</v8:Type> </Type> </Attribute> - <Attribute name="ФлагПлатформенный" id="42"> + <Attribute name="ФлагПлатформенный" id="46"> <Title> <v8:item> <v8:lang>ru</v8:lang> @@ -309,7 +380,7 @@ <v8:Type>xs:boolean</v8:Type> </Type> </Attribute> - <Attribute name="ФлагЯвный" id="43"> + <Attribute name="ФлагЯвный" id="47"> <Title> <v8:item> <v8:lang>ru</v8:lang> @@ -320,7 +391,7 @@ <v8:Type>xs:boolean</v8:Type> </Type> </Attribute> - <Attribute name="ФлагТумблер" id="44"> + <Attribute name="ФлагТумблер" id="48"> <Title> <v8:item> <v8:lang>ru</v8:lang>