From 819b7fa1263e26fe412b81dd9ec1b3508c285467 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Fri, 12 Jun 2026 15:00:19 +0300 Subject: [PATCH] =?UTF-8?q?feat(form-decompile,form-compile):=20dataParame?= =?UTF-8?q?ters=20=D0=B4=D0=B8=D0=BD=D1=81=D0=BF=D0=B8=D1=81=D0=BA=D0=B0?= =?UTF-8?q?=20+=20nil-=D0=B7=D0=BD=D0=B0=D1=87=D0=B5=D0=BD=D0=B8=D0=B5=20?= =?UTF-8?q?=D1=81=D1=85=D0=B5=D0=BC=D0=B0-=D0=BF=D0=B0=D1=80=D0=B0=D0=BC?= =?UTF-8?q?=D0=B5=D1=82=D1=80=D0=B0=20(valueListAllowed)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Раундтрип терял две вещи в настройках динамического списка реквизита (форма ВыборПодписантовПечатныхФорм): 1. — значения параметров запроса в ListSettings (список SettingsParameterValue {use, parameter, value?}). Не захватывались/не эмитились вовсе. 2. у схема-параметра при valueListAllowed=true: компилятор по умолчанию его пропускал (if valueListAllowed → return), но платформа пишет не всегда (корпус 27 с / 47 без). dataParameters: грамматика портирована из skd-compile (Emit-DataParameters + Parse-DataParamShorthand + Test/Emit-EmptyValue) — консистентно с СКД (shorthand "Имя @off" / объект). Form-нюанс: значение опционально (use=false плейсхолдер без value-узла, в отличие от skd-settings). compile эмитит после filter (XSD-порядок). decompile: Build-FormDataParameters (объект с полным valueType / shorthand). nil-значение: декомпилятор ставит явный маркер value:null при valueListAllowed+nil-тег; компилятор эмитит nil при valueListAllowed + явный value (различает absent от null через Has-DLProp/value_explicit). Корпус: dataParameters в 9 формах (17 items, все use=false, 4 со значениями DesignTimeValue/ent:/decimal). Верификация: таргет-раундтрип формы → match (22→0); 9 dataParameters-форм — категория закрыта (остаток — др. категории filter userSettingPresentation/MultipleValue*). Регресс form-compile 43/43 (ps1+py, PY-паритет снэпшота); 1С-cert dynamic-list-form (vla-nil + dataParameters грузятся). spec. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../form-compile/scripts/form-compile.ps1 | 137 +++++++++++++++- .../form-compile/scripts/form-compile.py | 154 +++++++++++++++++- .../form-decompile/scripts/form-decompile.ps1 | 53 +++++- docs/form-dsl-spec.md | 3 + .../cases/form-compile/dynamic-list-form.json | 2 + .../Товары/Forms/ФормаСписка/Ext/Form.xml | 21 +++ 6 files changed, 364 insertions(+), 6 deletions(-) diff --git a/.claude/skills/form-compile/scripts/form-compile.ps1 b/.claude/skills/form-compile/scripts/form-compile.ps1 index 0004c3bf..d53a3d6d 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.133 — Compile 1C managed form from JSON or object metadata +# form-compile v1.134 — Compile 1C managed form from JSON or object metadata # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [string]$JsonPath, @@ -5047,6 +5047,134 @@ function Emit-DLInputParameters { X "$indent" } +# ── dataParameters (значения параметров запроса в настройках компоновки) — порт из skd-compile ── +# Грамматика идентична СКД: shorthand "Имя = Значение @off @user" или объект +# {parameter, value?, valueType?, use?, nilValue?, viewMode?, userSettingID?, userSettingPresentation?}. +function Test-EmptyValue { + param($v) + if ($null -eq $v) { return $true } + $s = "$v".Trim() + if ($s -eq "") { return $true } + if ($s -eq "_") { return $true } + if ($s.ToLowerInvariant() -eq "null") { return $true } + return $false +} +function Emit-EmptyValue { + param([string]$type, [string]$indent, [string]$tagPrefix = "", [bool]$valueListAllowed = $false) + if ($valueListAllowed) { return } + $t = if ($null -eq $type) { "" } else { "$type" } + $tBare = if ($t -match '^xs:(.+)$') { $matches[1] } else { $t } + $pf = $tagPrefix + if ($t -eq "") { X "$indent<${pf}value xsi:nil=`"true`"/>" } + elseif ($t -eq "StandardPeriod") { + X "$indent<${pf}value xsi:type=`"v8:StandardPeriod`">" + X "$indent`tCustom" + X "$indent`t0001-01-01T00:00:00" + X "$indent`t0001-01-01T00:00:00" + X "$indent" + } + elseif ($tBare -match '^string') { X "$indent<${pf}value xsi:type=`"xs:string`"/>" } + elseif ($tBare -match '^(date|time)') { X "$indent<${pf}value xsi:type=`"xs:dateTime`">0001-01-01T00:00:00" } + elseif ($tBare -match '^decimal') { X "$indent<${pf}value xsi:type=`"xs:decimal`">0" } + elseif ($tBare -eq "boolean") { X "$indent<${pf}value xsi:type=`"xs:boolean`">false" } + else { X "$indent<${pf}value xsi:nil=`"true`"/>" } +} +function Parse-DataParamShorthand { + param([string]$s) + $result = @{ parameter = ""; value = $null; use = $true; userSettingID = $null; viewMode = $null } + if ($s -match '@user') { $result.userSettingID = "auto"; $s = $s -replace '\s*@user', '' } + if ($s -match '@off') { $result.use = $false; $s = $s -replace '\s*@off', '' } + if ($s -match '@quickAccess') { $result.viewMode = "QuickAccess"; $s = $s -replace '\s*@quickAccess', '' } + if ($s -match '@normal') { $result.viewMode = "Normal"; $s = $s -replace '\s*@normal', '' } + $s = $s.Trim() + if ($s -match '^([^=]+)=\s*(.+)$') { + $result.parameter = $Matches[1].Trim() + $valStr = $Matches[2].Trim() + $periodVariants = @("Custom","Today","ThisWeek","ThisTenDays","ThisMonth","ThisQuarter","ThisHalfYear","ThisYear","FromBeginningOfThisWeek","FromBeginningOfThisTenDays","FromBeginningOfThisMonth","FromBeginningOfThisQuarter","FromBeginningOfThisHalfYear","FromBeginningOfThisYear","LastWeek","LastTenDays","LastMonth","LastQuarter","LastHalfYear","LastYear","NextDay","NextWeek","NextTenDays","NextMonth","NextQuarter","NextHalfYear","NextYear","TillEndOfThisWeek","TillEndOfThisTenDays","TillEndOfThisMonth","TillEndOfThisQuarter","TillEndOfThisHalfYear","TillEndOfThisYear") + if ($periodVariants -contains $valStr) { $result.value = @{ variant = $valStr } } + elseif ($valStr -match '^\d{4}-\d{2}-\d{2}T') { $result.value = $valStr } + elseif ($valStr -eq "true" -or $valStr -eq "false") { $result.value = [bool]($valStr -eq "true") } + else { $result.value = $valStr } + } else { $result.parameter = $s } + return $result +} +function Emit-DataParameters { + param($items, [string]$indent, $blockViewMode = $null) + if (-not $items -or @($items).Count -eq 0) { return } + X "$indent" + foreach ($dp in @($items)) { + if ($dp -is [string]) { + $parsed = Parse-DataParamShorthand $dp + $dpObj = New-Object PSObject + $dpObj | Add-Member -NotePropertyName "parameter" -NotePropertyValue $parsed.parameter + if ($null -ne $parsed.value) { $dpObj | Add-Member -NotePropertyName "value" -NotePropertyValue $parsed.value } + if ($parsed.use -eq $false) { $dpObj | Add-Member -NotePropertyName "use" -NotePropertyValue $false } + if ($parsed.userSettingID) { $dpObj | Add-Member -NotePropertyName "userSettingID" -NotePropertyValue $parsed.userSettingID } + if ($parsed.viewMode) { $dpObj | Add-Member -NotePropertyName "viewMode" -NotePropertyValue $parsed.viewMode } + $dp = $dpObj + } + X "$indent`t" + if ($dp.use -eq $false) { X "$indent`t`tfalse" } + X "$indent`t`t$(Esc-Xml "$($dp.parameter)")" + if ($dp.nilValue -eq $true) { + X "$indent`t`t" + } elseif ((Test-EmptyValue $dp.value) -and $dp.valueType) { + # Явный типизированный пустой (xs:string-плейсхолдер и т.п.) + Emit-EmptyValue -type "$($dp.valueType)" -indent "$indent`t`t" -tagPrefix "dcscor:" -valueListAllowed $false + } elseif (Test-EmptyValue $dp.value) { + # Нет значения и нет valueType → НЕ эмитим value-узел (form дин-список: use=false плейсхолдер). + # (В отличие от skd-settings, где значение всегда присутствует.) + } elseif ($null -ne $dp.value) { + $vtype = "$($dp.valueType)" + if (($dp.value -is [PSCustomObject] -or $dp.value -is [hashtable] -or $dp.value -is [System.Collections.IDictionary]) -and ($dp.value.variant)) { + $_hasDate = $false; $_hasSD = $false + if ($dp.value -is [PSCustomObject]) { $_hasDate = [bool]$dp.value.PSObject.Properties['date']; $_hasSD = [bool]$dp.value.PSObject.Properties['startDate'] } + else { $_hasDate = $dp.value.Contains('date'); $_hasSD = $dp.value.Contains('startDate') } + $_variantStr = "$($dp.value.variant)" + $_isSBD = $_hasDate -or (-not $_hasSD -and $_variantStr -like 'BeginningOf*') + if ($_isSBD) { + $_d = $null + if ($dp.value -is [PSCustomObject] -and $dp.value.PSObject.Properties['date']) { $_d = "$($dp.value.date)" } + elseif (($dp.value -is [System.Collections.IDictionary]) -and $dp.value.Contains('date')) { $_d = "$($dp.value['date'])" } + X "$indent`t`t" + X "$indent`t`t`t$(Esc-Xml $_variantStr)" + if ($_variantStr -eq 'Custom') { if (-not $_d) { $_d = '0001-01-01T00:00:00' }; X "$indent`t`t`t$(Esc-Xml $_d)" } + X "$indent`t`t" + } else { + $_sd = $null; $_ed = $null + if ($dp.value -is [PSCustomObject]) { if ($dp.value.PSObject.Properties['startDate']) { $_sd = "$($dp.value.startDate)" }; if ($dp.value.PSObject.Properties['endDate']) { $_ed = "$($dp.value.endDate)" } } + else { if ($dp.value.Contains('startDate')) { $_sd = "$($dp.value['startDate'])" }; if ($dp.value.Contains('endDate')) { $_ed = "$($dp.value['endDate'])" } } + X "$indent`t`t" + X "$indent`t`t`t$(Esc-Xml $_variantStr)" + if ($_variantStr -eq 'Custom') { if (-not $_sd) { $_sd = '0001-01-01T00:00:00' }; if (-not $_ed) { $_ed = '0001-01-01T00:00:00' }; X "$indent`t`t`t$(Esc-Xml $_sd)"; X "$indent`t`t`t$(Esc-Xml $_ed)" } + X "$indent`t`t" + } + } elseif ($vtype -match '^[a-zA-Z]+:') { + $vStr = if ($dp.value -is [bool]) { "$($dp.value)".ToLower() } else { "$($dp.value)" } + X "$indent`t`t$(Esc-Xml $vStr)" + } elseif ($vtype -eq 'boolean' -or $dp.value -is [bool]) { + X "$indent`t`t$(Esc-Xml ("$($dp.value)".ToLower()))" + } elseif ($vtype -match '^date' -or "$($dp.value)" -match '^\d{4}-\d{2}-\d{2}T') { + X "$indent`t`t$(Esc-Xml "$($dp.value)")" + } elseif ($vtype -match '^decimal') { + X "$indent`t`t$(Esc-Xml "$($dp.value)")" + } elseif ($vtype -match '^string') { + X "$indent`t`t$(Esc-Xml "$($dp.value)")" + } elseif ("$($dp.value)" -match '^(ПланСчетов|Справочник|Перечисление|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена)\.' -or "$($dp.value)" -match '^(ChartOfAccounts|Catalog|Enum|Document|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.') { + X "$indent`t`t$(Esc-Xml "$($dp.value)")" + } else { + X "$indent`t`t$(Esc-Xml "$($dp.value)")" + } + } + if ($dp.viewMode) { X "$indent`t`t$(Esc-Xml "$($dp.viewMode)")" } + if ($dp.userSettingID) { $uid = if ("$($dp.userSettingID)" -eq "auto") { New-Guid-String } else { "$($dp.userSettingID)" }; X "$indent`t`t$(Esc-Xml $uid)" } + if ($dp.userSettingPresentation) { Emit-MLText -tag "dcsset:userSettingPresentation" -text $dp.userSettingPresentation -indent "$indent`t`t" } + X "$indent`t" + } + if ($null -ne $blockViewMode) { X "$indent`t$(Esc-Xml "$blockViewMode")" } + X "$indent" +} + function Emit-DLParameter { param($p, $parsed, [string]$indent) X "$indent" @@ -5066,6 +5194,9 @@ function Emit-DLParameter { $valIsArray = ($parsed.value -is [array]) -or ($parsed.value -is [System.Collections.IList] -and $parsed.value -isnot [string]) if ($valIsArray) { foreach ($v in @($parsed.value)) { Emit-DLValue -type $parsed.type -val $v -indent $ci -valueListAllowed $false } + } elseif ($vla -and (Test-DLEmptyValue $parsed.value) -and $parsed.valueExplicit) { + # valueListAllowed + явный пустой (value:null от декомпилятора) → платформа здесь пишет nil + X "$ci" } else { Emit-DLValue -type $parsed.type -val $parsed.value -indent $ci -valueListAllowed $vla } @@ -5114,7 +5245,7 @@ function Emit-DLParameters { } elseif ((Has-DLProp $p 'valueType') -and $p.valueType) { $resolvedType = Resolve-TypeStr "$($p.valueType)" } - $parsed = @{ name = "$($p.name)"; type = $resolvedType; value = $(if (Has-DLProp $p 'value') { $p.value } else { $null }); title = $null } + $parsed = @{ name = "$($p.name)"; type = $resolvedType; value = $(if (Has-DLProp $p 'value') { $p.value } else { $null }); valueExplicit = (Has-DLProp $p 'value'); title = $null } if ((Has-DLProp $p 'valueListAllowed') -and $p.valueListAllowed -eq $true) { $parsed.valueListAllowed = $true } if ((Has-DLProp $p 'hidden') -and $p.hidden -eq $true) { $parsed.hidden = $true } } @@ -5355,6 +5486,8 @@ function Emit-Attributes { } else { # Полный каноничный скелет (умолчание, ~93% форм) — без изменений. Emit-Filter -items $st.filter -indent $lsi -blockViewMode 'Normal' -blockUserSettingID $script:CANON_FILTER_ID + # dataParameters — после filter, до order (XSD-порядок ListSettings) + if ($st.PSObject.Properties['dataParameters']) { Emit-DataParameters -items $st.dataParameters -indent $lsi } Emit-Order -items $st.order -indent $lsi -blockViewMode 'Normal' -blockUserSettingID $script:CANON_ORDER_ID Emit-ConditionalAppearance -items $st.conditionalAppearance -indent $lsi -blockViewMode 'Normal' -blockUserSettingID $script:CANON_CA_ID X "$lsiNormal" diff --git a/.claude/skills/form-compile/scripts/form-compile.py b/.claude/skills/form-compile/scripts/form-compile.py index b6e7c27e..3b5280a9 100644 --- a/.claude/skills/form-compile/scripts/form-compile.py +++ b/.claude/skills/form-compile/scripts/form-compile.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# form-compile v1.133 — Compile 1C managed form from JSON or object metadata +# form-compile v1.134 — Compile 1C managed form from JSON or object metadata # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import copy @@ -4786,6 +4786,149 @@ def emit_dl_input_parameters(lines, ip, indent): lines.append(f'{indent}') +# ── dataParameters (значения параметров запроса в настройках компоновки) — порт из skd ── +def _test_empty_value(v): + if v is None: + return True + s = str(v).strip() + return s == '' or s == '_' or s.lower() == 'null' + + +def emit_empty_value(lines, type_str, indent, tag_prefix='', value_list_allowed=False): + if value_list_allowed: + return + t = type_str or '' + t_bare = t[3:] if t.startswith('xs:') else t + pf = tag_prefix + if t == '': + lines.append(f'{indent}<{pf}value xsi:nil="true"/>') + elif t == 'StandardPeriod': + lines.append(f'{indent}<{pf}value xsi:type="v8:StandardPeriod">') + lines.append(f'{indent}\tCustom') + lines.append(f'{indent}\t0001-01-01T00:00:00') + lines.append(f'{indent}\t0001-01-01T00:00:00') + lines.append(f'{indent}') + elif re.match(r'^string', t_bare): + lines.append(f'{indent}<{pf}value xsi:type="xs:string"/>') + elif re.match(r'^(date|time)', t_bare): + lines.append(f'{indent}<{pf}value xsi:type="xs:dateTime">0001-01-01T00:00:00') + elif re.match(r'^decimal', t_bare): + lines.append(f'{indent}<{pf}value xsi:type="xs:decimal">0') + elif t_bare == 'boolean': + lines.append(f'{indent}<{pf}value xsi:type="xs:boolean">false') + else: + lines.append(f'{indent}<{pf}value xsi:nil="true"/>') + + +_DP_PERIOD_VARIANTS = {"Custom","Today","ThisWeek","ThisTenDays","ThisMonth","ThisQuarter","ThisHalfYear","ThisYear","FromBeginningOfThisWeek","FromBeginningOfThisTenDays","FromBeginningOfThisMonth","FromBeginningOfThisQuarter","FromBeginningOfThisHalfYear","FromBeginningOfThisYear","LastWeek","LastTenDays","LastMonth","LastQuarter","LastHalfYear","LastYear","NextDay","NextWeek","NextTenDays","NextMonth","NextQuarter","NextHalfYear","NextYear","TillEndOfThisWeek","TillEndOfThisTenDays","TillEndOfThisMonth","TillEndOfThisQuarter","TillEndOfThisHalfYear","TillEndOfThisYear"} + + +def parse_data_param_shorthand(s): + result = {'parameter': '', 'value': None, 'use': True, 'userSettingID': None, 'viewMode': None} + if '@user' in s: + result['userSettingID'] = 'auto'; s = re.sub(r'\s*@user', '', s) + if '@off' in s: + result['use'] = False; s = re.sub(r'\s*@off', '', s) + if '@quickAccess' in s: + result['viewMode'] = 'QuickAccess'; s = re.sub(r'\s*@quickAccess', '', s) + if '@normal' in s: + result['viewMode'] = 'Normal'; s = re.sub(r'\s*@normal', '', s) + s = s.strip() + m = re.match(r'^([^=]+)=\s*(.+)$', s) + if m: + result['parameter'] = m.group(1).strip() + val_str = m.group(2).strip() + if val_str in _DP_PERIOD_VARIANTS: + result['value'] = {'variant': val_str} + elif re.match(r'^\d{4}-\d{2}-\d{2}T', val_str): + result['value'] = val_str + elif val_str in ('true', 'false'): + result['value'] = (val_str == 'true') + else: + result['value'] = val_str + else: + result['parameter'] = s + return result + + +def emit_data_parameters(lines, items, indent, block_view_mode=None): + if not items or len(items) == 0: + return + lines.append(f'{indent}') + for dp in items: + if isinstance(dp, str): + parsed = parse_data_param_shorthand(dp) + dp = {'parameter': parsed['parameter']} + if parsed['value'] is not None: + dp['value'] = parsed['value'] + if parsed['use'] is False: + dp['use'] = False + if parsed['userSettingID']: + dp['userSettingID'] = parsed['userSettingID'] + if parsed['viewMode']: + dp['viewMode'] = parsed['viewMode'] + lines.append(f'{indent}\t') + if dp.get('use') is False: + lines.append(f'{indent}\t\tfalse') + lines.append(f'{indent}\t\t{esc_xml(str(dp.get("parameter", "")))}') + vtype = str(dp.get('valueType') or '') + val = dp.get('value') + if dp.get('nilValue') is True: + lines.append(f'{indent}\t\t') + elif _test_empty_value(val) and vtype: + emit_empty_value(lines, vtype, f'{indent}\t\t', tag_prefix='dcscor:', value_list_allowed=False) + elif _test_empty_value(val): + pass # нет значения → не эмитим value-узел (form дин-список: use=false плейсхолдер) + elif val is not None: + if isinstance(val, dict) and val.get('variant'): + variant = str(val.get('variant')) + has_date = 'date' in val + has_sd = 'startDate' in val + is_sbd = has_date or (not has_sd and variant.startswith('BeginningOf')) + if is_sbd: + lines.append(f'{indent}\t\t') + lines.append(f'{indent}\t\t\t{esc_xml(variant)}') + if variant == 'Custom': + d = str(val.get('date') or '0001-01-01T00:00:00') + lines.append(f'{indent}\t\t\t{esc_xml(d)}') + lines.append(f'{indent}\t\t') + else: + lines.append(f'{indent}\t\t') + lines.append(f'{indent}\t\t\t{esc_xml(variant)}') + if variant == 'Custom': + sd = str(val.get('startDate') or '0001-01-01T00:00:00') + ed = str(val.get('endDate') or '0001-01-01T00:00:00') + lines.append(f'{indent}\t\t\t{esc_xml(sd)}') + lines.append(f'{indent}\t\t\t{esc_xml(ed)}') + lines.append(f'{indent}\t\t') + elif re.match(r'^[a-zA-Z]+:', vtype): + v_str = str(val).lower() if isinstance(val, bool) else str(val) + lines.append(f'{indent}\t\t{esc_xml(v_str)}') + elif vtype == 'boolean' or isinstance(val, bool): + lines.append(f'{indent}\t\t{esc_xml(str(val).lower())}') + elif re.match(r'^date', vtype) or re.match(r'^\d{4}-\d{2}-\d{2}T', str(val)): + lines.append(f'{indent}\t\t{esc_xml(str(val))}') + elif re.match(r'^decimal', vtype): + lines.append(f'{indent}\t\t{esc_xml(str(val))}') + elif re.match(r'^string', vtype): + lines.append(f'{indent}\t\t{esc_xml(str(val))}') + elif re.match(r'^(ПланСчетов|Справочник|Перечисление|Документ|ПланВидовХарактеристик|ПланВидовРасчета|БизнесПроцесс|Задача|РегистрСведений|ПланОбмена)\.', str(val)) or re.match(r'^(ChartOfAccounts|Catalog|Enum|Document|ChartOfCharacteristicTypes|ChartOfCalculationTypes|BusinessProcess|Task|InformationRegister|ExchangePlan)\.', str(val)): + lines.append(f'{indent}\t\t{esc_xml(str(val))}') + else: + lines.append(f'{indent}\t\t{esc_xml(str(val))}') + if dp.get('viewMode'): + lines.append(f'{indent}\t\t{esc_xml(str(dp["viewMode"]))}') + if dp.get('userSettingID'): + uid = new_uuid() if str(dp['userSettingID']) == 'auto' else str(dp['userSettingID']) + lines.append(f'{indent}\t\t{esc_xml(uid)}') + if dp.get('userSettingPresentation'): + emit_mltext(lines, f'{indent}\t\t', 'dcsset:userSettingPresentation', dp['userSettingPresentation']) + lines.append(f'{indent}\t') + if block_view_mode is not None: + lines.append(f'{indent}\t{esc_xml(str(block_view_mode))}') + lines.append(f'{indent}') + + def emit_dl_parameter(lines, p, parsed, indent): is_obj = not isinstance(p, str) lines.append(f'{indent}') @@ -4811,6 +4954,9 @@ def emit_dl_parameter(lines, p, parsed, indent): if isinstance(pv, list): for v in pv: emit_dl_value(lines, parsed.get('type', ''), v, ci, False) + elif vla and is_dl_empty_value(pv) and parsed.get('value_explicit'): + # valueListAllowed + явный пустой (value:null от декомпилятора) → платформа пишет nil + lines.append(f'{ci}') else: emit_dl_value(lines, parsed.get('type', ''), pv, ci, vla) # useRestriction — ВСЕГДА; дефолт true; false только при явном useRestriction:false. @@ -4865,7 +5011,8 @@ def emit_dl_parameters(lines, params, indent): elif p.get('valueType'): resolved_type = resolve_type_str(str(p['valueType'])) parsed = {'name': str(p.get('name', '')), 'type': resolved_type, - 'value': p.get('value') if 'value' in p else None, 'title': None} + 'value': p.get('value') if 'value' in p else None, + 'value_explicit': ('value' in p), 'title': None} if p.get('valueListAllowed') is True: parsed['valueListAllowed'] = True if p.get('hidden') is True: @@ -5094,6 +5241,9 @@ def emit_attributes(lines, attrs, indent, conditional_appearance=None): else: # Полный каноничный скелет (умолчание, ~93% форм) — без изменений. emit_filter(lines, s.get('filter'), lsi, block_view_mode='Normal', block_user_setting_id=CANON_FILTER_ID) + # dataParameters — после filter, до order (XSD-порядок ListSettings) + if 'dataParameters' in s: + emit_data_parameters(lines, s.get('dataParameters'), lsi) emit_order(lines, s.get('order'), lsi, block_view_mode='Normal', block_user_setting_id=CANON_ORDER_ID) emit_conditional_appearance(lines, s.get('conditionalAppearance'), lsi, block_view_mode='Normal', block_user_setting_id=CANON_CA_ID) lines.append(f'{lsi}Normal') diff --git a/.claude/skills/form-decompile/scripts/form-decompile.ps1 b/.claude/skills/form-decompile/scripts/form-decompile.ps1 index be13bf9d..0ee93982 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.107 — Decompile 1C managed Form.xml to JSON DSL (draft) +# form-decompile v0.108 — Decompile 1C managed Form.xml to JSON DSL (draft) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # ВНИМАНИЕ: раундтрип не гарантируется. Навык исключён из авто-использования моделью. param( @@ -1231,6 +1231,44 @@ function Build-DLInputParameters { return @($items) } +# dcsset:dataParameters → массив (shorthand "Имя @off" для value-less / объект для типизированного +# значения). Грамматика зеркалит skd-compile Emit-DataParameters (form-контекст: значение опционально, +# в отличие от skd-settings). Без «auto»-компактизации (нужна машинерия сравнения с top-level). +function Build-FormDataParameters { + param($dpNode) + $entries = @() + foreach ($it in @($dpNode.SelectNodes("dcscor:item", $ns))) { + $pn = Get-Text $it "dcscor:parameter" + $use = Get-Text $it "dcscor:use" + $valNode = $it.SelectSingleNode("dcscor:value", $ns) + $usidN = $it.SelectSingleNode("dcsset:userSettingID", $ns) + $vmN = $it.SelectSingleNode("dcsset:viewMode", $ns) + $uspN = $it.SelectSingleNode("dcsset:userSettingPresentation", $ns) + if ($valNode -or $usidN -or $vmN -or $uspN) { + $obj = [ordered]@{ parameter = $pn } + if ($valNode) { + if ($valNode.GetAttribute("nil", $NS_XSI) -eq 'true') { $obj['nilValue'] = $true } + else { + $vType = $valNode.GetAttribute("type", $NS_XSI); $vVal = $valNode.InnerText + if ($vType -match 'decimal$' -and $vVal -match '^-?\d+$') { $obj['value'] = [int]$vVal } + elseif ($vType -match 'boolean$') { $obj['value'] = ($vVal -eq 'true') } + else { $obj['value'] = $vVal } + if ($vType) { $obj['valueType'] = $vType } + } + } + if ($use -eq 'false') { $obj['use'] = $false } + if ($usidN) { $obj['userSettingID'] = 'auto' } + if ($vmN) { $obj['viewMode'] = $vmN.InnerText } + if ($uspN) { $usp = Get-MLText $uspN; if ($null -ne $usp) { $obj['userSettingPresentation'] = $usp } } + $entries += $obj + } else { + $s = $pn; if ($use -eq 'false') { $s += ' @off' } + $entries += $s + } + } + return ,$entries +} + function Build-DLParameter { param($pNode) $name = Get-Child $pNode 'name' @@ -1249,10 +1287,14 @@ function Build-DLParameter { $vtNode = $pNode.SelectSingleNode("dcssch:valueType", $ns) $typeVal = $null if ($vtNode) { $typeVal = Decompile-Type $vtNode; if ($typeVal) { $o['type'] = $typeVal } } - # value — опускаем nil (дефолт) + # value — опускаем nil (дефолт), КРОМЕ valueListAllowed+nil: платформа пишет + # не всегда (корпус 27 с / 47 без), а компилятор при valueListAllowed по умолчанию его НЕ эмитит → + # явный маркер value:null, чтобы реэмитить nil. (Различается через Has-DLProp в компиляторе.) $vNode = $pNode.SelectSingleNode("dcssch:value", $ns) if ($vNode -and ($vNode.GetAttribute("nil", $NS_XSI) -ne 'true')) { $o['value'] = Convert-TypedValue -raw $vNode.InnerText -xsiType ($vNode.GetAttribute("type", $NS_XSI)) + } elseif ($vNode -and ((Get-Child $pNode 'valueListAllowed') -eq 'true')) { + $o['value'] = $null } # useRestriction — опускаем true (дефолт), фиксируем false if ((Get-Child $pNode 'useRestriction') -eq 'false') { $o['useRestriction'] = $false } @@ -2499,6 +2541,13 @@ if ($attrsNode) { $ca = Build-ConditionalAppearance -caNode $caNode -loc "settings/conditionalAppearance" if (@($ca).Count -gt 0) { $so['conditionalAppearance'] = @($ca) } } + # Параметры данных компоновки (dcsset:dataParameters) — значения параметров запроса в + # настройках. Грамматика как у СКД (shorthand "Имя @off" / объект). См. Build-FormDataParameters. + $dpNode = $lsNode.SelectSingleNode("dcsset:dataParameters", $ns) + if ($dpNode -and $dpNode.SelectSingleNode("dcscor:item", $ns)) { + $dp = Build-FormDataParameters $dpNode + if (@($dp).Count -gt 0) { $so['dataParameters'] = @($dp) } + } # Форма скелета ListSettings: дескриптор только для НЕ-каноничных форм (частичные/минимальные). # Канон → $null (компилятор регенерит полный скелет, как раньше). $lsShape = Get-ListSettingsShape $lsNode diff --git a/docs/form-dsl-spec.md b/docs/form-dsl-spec.md index ee561597..9a512759 100644 --- a/docs/form-dsl-spec.md +++ b/docs/form-dsl-spec.md @@ -939,6 +939,7 @@ Forgiving-синонимы типа: XML-имя (`SpreadSheetDocumentField`) и | `parameters` | array | Параметры схемы запроса (`DataCompositionSchemaParameter`) — см. ниже | | `order` | array | Сортировка списка (см. ниже) | | `filter` | array | Отбор списка (грамматика как в СКД) | +| `dataParameters` | array | Значения параметров запроса в настройках (``). **Грамматика как в СКД**: shorthand `"Имя = Значение @off @user"` или объект `{ parameter, value?, valueType?, use?, nilValue?, viewMode?, userSettingID?, userSettingPresentation? }`. В дин-списке частый паттерн — плейсхолдер отключённого параметра без значения: `"ИмяПараметра @off"` | | `conditionalAppearance` | array | Условное оформление списка (грамматика как в СКД) | `ManualQuery` выводится из наличия `query` — отдельным ключом не задаётся. @@ -973,6 +974,8 @@ Forgiving-синонимы типа: XML-имя (`SpreadSheetDocumentField`) и Объектные ключи (как в СКД): `name`, `title`, `type`/`valueType`, `value`, `valueListAllowed`, `useRestriction`, `availableAsField`, `expression`, `availableValues` (`[{ value, presentation }]`), `inputParameters`, `denyIncompleteValues`, `use`. +> **`value: null` при `valueListAllowed: true`** — явный маркер «эмитить ``». Платформа пишет nil-значение для valueListAllowed-параметра не всегда (корпус 27 с / 47 без); по умолчанию (ключ `value` отсутствует) компилятор его НЕ эмитит. Декомпилятор ставит `value: null`, когда оригинал содержит nil-тег. + #### order / filter / conditionalAppearance Грамматика этих ключей идентична настройкам СКД — см. [skd-dsl-spec.md](skd-dsl-spec.md) (разделы filter / order / conditionalAppearance). Кратко: diff --git a/tests/skills/cases/form-compile/dynamic-list-form.json b/tests/skills/cases/form-compile/dynamic-list-form.json index 8d03f38b..5cac52ab 100644 --- a/tests/skills/cases/form-compile/dynamic-list-form.json +++ b/tests/skills/cases/form-compile/dynamic-list-form.json @@ -19,8 +19,10 @@ "attributes": [ { "name": "Список", "type": "DynamicList", "useAlways": ["~Артикул", "Список.Code", "Description"], "settings": { "mainTable": "Catalog.Товары", "dynamicDataRead": true, "autoSaveUserSettings": false, + "parameters": [ { "name": "ВыборТовара", "type": "CatalogRef.Товары", "valueListAllowed": true, "value": null } ], "order": [ "Description", "Code desc" ], "filter": [ "Артикул = _ @off @user", "Артикул like %тест%", "Description подобно %abc%" ], + "dataParameters": [ "ВыборТовара @off" ], "conditionalAppearance": [ { "filter": ["Артикул = _"], "appearance": { "ЦветТекста": "web:Red" } } ] } } ], diff --git a/tests/skills/cases/form-compile/snapshots/dynamic-list-form/Catalogs/Товары/Forms/ФормаСписка/Ext/Form.xml b/tests/skills/cases/form-compile/snapshots/dynamic-list-form/Catalogs/Товары/Forms/ФормаСписка/Ext/Form.xml index c52430b9..9db56f5f 100644 --- a/tests/skills/cases/form-compile/snapshots/dynamic-list-form/Catalogs/Товары/Forms/ФормаСписка/Ext/Form.xml +++ b/tests/skills/cases/form-compile/snapshots/dynamic-list-form/Catalogs/Товары/Forms/ФормаСписка/Ext/Form.xml @@ -100,6 +100,21 @@ false true + + ВыборТовара + + + ru + Выбор товара + + + + cfg:CatalogRef.Товары + + + true + true + Catalog.Товары false @@ -123,6 +138,12 @@ Normal UUID-003 + + + false + ВыборТовара + + Description