diff --git a/.claude/skills/form-compile/scripts/form-compile.ps1 b/.claude/skills/form-compile/scripts/form-compile.ps1 index cc4157e6..b79c0a87 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.138 — Compile 1C managed form from JSON or object metadata +# form-compile v1.139 — Compile 1C managed form from JSON or object metadata # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [string]$JsonPath, @@ -2006,6 +2006,67 @@ function Emit-ConditionalAppearance { X "$indent" } +# === Группировка строк динамического списка (DCS-структура ListSettings) === +# Линейная цепочка (каждый уровень = одно поле в groupItems; +# вложенность — через дочерний ). Зеркало skd Emit-GroupItems/Emit-StructureItem, +# но плоская модель уровней (список всегда линеен, без selection/order/children). +function Get-ListGroupingValue { + param($st) + foreach ($k in 'grouping','structure','группировка') { + if ($st.PSObject.Properties[$k] -and $st.$k) { return $st.$k } + } + return $null +} + +function Parse-ListGrouping { + param($grouping) + # Шорткат "A > B > C" → массив имён; массив строк/объектов → как есть. + # Unary comma: иначе PS разворачивает одноэлементный массив при return → строка → индексация даёт char. + if (-not $grouping) { return ,@() } + if ($grouping -is [string]) { return ,@($grouping -split '\s*>\s*' | Where-Object { "$_" -ne '' }) } + return ,@($grouping) +} + +function Emit-GroupItemField { + param($level, [string]$indent) + if ($level -is [string]) { + $field = $level; $gt = 'Items'; $pat = 'None'; $pab = '0001-01-01T00:00:00'; $pae = '0001-01-01T00:00:00' + } else { + $field = "$($level.field)" + $gt = if ($level.groupType) { "$($level.groupType)" } else { 'Items' } + $pat = if ($level.periodAdditionType) { "$($level.periodAdditionType)" } else { 'None' } + $pab = if ($level.periodAdditionBegin) { "$($level.periodAdditionBegin)" } else { '0001-01-01T00:00:00' } + $pae = if ($level.periodAdditionEnd) { "$($level.periodAdditionEnd)" } else { '0001-01-01T00:00:00' } + } + X "$indent" + X "$indent`t$(Esc-Xml $field)" + X "$indent`t$(Esc-Xml $gt)" + X "$indent`t$(Esc-Xml $pat)" + # Авто-детект: ISO-дата → xs:dateTime, иначе путь → dcscor:Field. + $pabT = if ($pab -match '^\d{4}-\d{2}-\d{2}T') { 'xs:dateTime' } else { 'dcscor:Field' } + $paeT = if ($pae -match '^\d{4}-\d{2}-\d{2}T') { 'xs:dateTime' } else { 'dcscor:Field' } + X "$indent`t$(Esc-Xml $pab)" + X "$indent`t$(Esc-Xml $pae)" + X "$indent" +} + +function Emit-ListGroupingLevels { + param($levels, [int]$i, [string]$indent) + X "$indent" + X "$indent`t" + Emit-GroupItemField $levels[$i] "$indent`t`t" + X "$indent`t" + if ($i -lt $levels.Count - 1) { Emit-ListGroupingLevels $levels ($i + 1) "$indent`t" } + X "$indent" +} + +function Emit-ListGrouping { + param($grouping, [string]$indent) + $levels = Parse-ListGrouping $grouping + if ($levels.Count -eq 0) { return } + Emit-ListGroupingLevels $levels 0 $indent +} + # --- 5. Type emitter --- $script:formTypeSynonyms = New-Object System.Collections.Hashtable @@ -5517,6 +5578,7 @@ function Emit-Attributes { 'conditionalAppearance' { $bus = if ($meta -match 'u') { $script:CANON_CA_ID } else { $null }; Emit-ConditionalAppearance -items $st.conditionalAppearance -indent $lsi -blockViewMode $bvm -blockUserSettingID $bus } 'itemsViewMode' { X "$lsiNormal" } 'itemsUserSettingID' { X "$lsi$($script:CANON_ITEMS_ID)" } + 'structure' { Emit-ListGrouping (Get-ListGroupingValue $st) $lsi } } } } else { @@ -5526,6 +5588,8 @@ function Emit-Attributes { 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 + # Группировка строк списка (авторинг без round-trip дескриптора) — после CA, до itemsViewMode + Emit-ListGrouping (Get-ListGroupingValue $st) $lsi X "$lsiNormal" X "$lsi$($script:CANON_ITEMS_ID)" } diff --git a/.claude/skills/form-compile/scripts/form-compile.py b/.claude/skills/form-compile/scripts/form-compile.py index ab582577..6642700f 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.138 — Compile 1C managed form from JSON or object metadata +# form-compile v1.139 — Compile 1C managed form from JSON or object metadata # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import copy @@ -1689,6 +1689,64 @@ def emit_appearance_value(lines, key, val, indent): lines.append(f'{indent}') +# === Группировка строк динамического списка (DCS-структура ListSettings) === +# Линейная цепочка (каждый уровень = одно поле в groupItems; +# вложенность — через дочерний ). Плоская модель уровней (список всегда линеен). +def get_list_grouping_value(s): + for k in ('grouping', 'structure', 'группировка'): + if s.get(k): + return s[k] + return None + + +def parse_list_grouping(grouping): + # Шорткат "A > B > C" → массив имён; массив строк/объектов → как есть. + if not grouping: + return [] + if isinstance(grouping, str): + return [p.strip() for p in re.split(r'\s*>\s*', grouping) if p.strip()] + return list(grouping) + + +def emit_group_item_field(lines, level, indent): + if isinstance(level, str): + field, gt, pat = level, 'Items', 'None' + pab = pae = '0001-01-01T00:00:00' + else: + field = str(level.get('field', '')) + gt = str(level.get('groupType') or 'Items') + pat = str(level.get('periodAdditionType') or 'None') + pab = str(level.get('periodAdditionBegin') or '0001-01-01T00:00:00') + pae = str(level.get('periodAdditionEnd') or '0001-01-01T00:00:00') + lines.append(f'{indent}') + lines.append(f'{indent}\t{esc_xml(field)}') + lines.append(f'{indent}\t{esc_xml(gt)}') + lines.append(f'{indent}\t{esc_xml(pat)}') + # Авто-детект: ISO-дата → xs:dateTime, иначе путь → dcscor:Field. + pab_t = 'xs:dateTime' if re.match(r'^\d{4}-\d{2}-\d{2}T', pab) else 'dcscor:Field' + pae_t = 'xs:dateTime' if re.match(r'^\d{4}-\d{2}-\d{2}T', pae) else 'dcscor:Field' + lines.append(f'{indent}\t{esc_xml(pab)}') + lines.append(f'{indent}\t{esc_xml(pae)}') + lines.append(f'{indent}') + + +def emit_list_grouping_levels(lines, levels, i, indent): + lines.append(f'{indent}') + lines.append(f'{indent}\t') + emit_group_item_field(lines, levels[i], f'{indent}\t\t') + lines.append(f'{indent}\t') + if i < len(levels) - 1: + emit_list_grouping_levels(lines, levels, i + 1, f'{indent}\t') + lines.append(f'{indent}') + + +def emit_list_grouping(lines, grouping, indent): + levels = parse_list_grouping(grouping) + if not levels: + return + emit_list_grouping_levels(lines, levels, 0, indent) + + def emit_conditional_appearance(lines, items, indent, block_view_mode=None, block_user_setting_id=None, wrap_tag='dcsset:conditionalAppearance'): has_items = bool(items) and len(items) > 0 has_block_meta = (block_view_mode is not None) or (block_user_setting_id is not None) @@ -5262,6 +5320,8 @@ def emit_attributes(lines, attrs, indent, conditional_appearance=None): lines.append(f'{lsi}Normal') elif tag == 'itemsUserSettingID': lines.append(f'{lsi}{CANON_ITEMS_ID}') + elif tag == 'structure': + emit_list_grouping(lines, get_list_grouping_value(s), lsi) else: # Полный каноничный скелет (умолчание, ~93% форм) — без изменений. emit_filter(lines, s.get('filter'), lsi, block_view_mode='Normal', block_user_setting_id=CANON_FILTER_ID) @@ -5270,6 +5330,8 @@ def emit_attributes(lines, attrs, indent, conditional_appearance=None): 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) + # Группировка строк списка (авторинг без round-trip дескриптора) — после CA, до itemsViewMode + emit_list_grouping(lines, get_list_grouping_value(s), lsi) lines.append(f'{lsi}Normal') lines.append(f'{lsi}{CANON_ITEMS_ID}') if len(lines) - 1 == ls_open_idx: diff --git a/.claude/skills/form-decompile/scripts/form-decompile.ps1 b/.claude/skills/form-decompile/scripts/form-decompile.ps1 index f5a5d0c6..1f2d0d74 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.111 — Decompile 1C managed Form.xml to JSON DSL (draft) +# form-decompile v0.112 — Decompile 1C managed Form.xml to JSON DSL (draft) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # ВНИМАНИЕ: раундтрип не гарантируется. Навык исключён из авто-использования моделью. param( @@ -118,7 +118,7 @@ function Test-ListSettingsHasContent { # полному каноничному скелету (компилятор регенерит сам) ИЛИ содержит неподдержанные top-level элементы # (item/dataParameters/viewMode/userSettingID/… → fallback на канон). Иначе — дескриптор для компилятора. function Get-ListSettingsShape { - param($lsNode) + param($lsNode, [bool]$hasGrouping = $false) if (-not $lsNode) { return $null } $shape = [ordered]@{} foreach ($child in $lsNode.ChildNodes) { @@ -130,7 +130,8 @@ function Get-ListSettingsShape { $shape[$tag] = "$(if ($hasVM) {'v'})$(if ($hasUS) {'u'})" } elseif ($tag -eq 'itemsViewMode') { $shape['itemsViewMode'] = $true } elseif ($tag -eq 'itemsUserSettingID') { $shape['itemsUserSettingID'] = $true } - else { return $null } # item/dataParameters/itemsUserSettingPresentation/… → канон-fallback + elseif ($tag -eq 'item') { if ($hasGrouping) { $shape['structure'] = $true } else { return $null } } + else { return $null } # dataParameters/itemsUserSettingPresentation/… → канон-fallback } # Полный каноничный скелет → опускаем (компилятор регенерит) if ($shape.Count -eq 5 -and $shape['filter'] -eq 'vu' -and $shape['order'] -eq 'vu' -and ` @@ -138,6 +139,60 @@ function Get-ListSettingsShape { return $shape } +# Группировка строк динамического списка: цепочка (каждый уровень = +# один groupItems-field; вложенность через дочерний ). Возвращает ПЛОСКИЙ массив уровней +# (string для дефолтного поля; объект {field,groupType?,periodAdditionType?,periodAdditionBegin?,periodAdditionEnd?} +# для нестандартного) ИЛИ $null, если структура не «чистая линейная цепочка одно-польных уровней» +# (ветвление/мультиполе/доп.содержимое) → честный fallback на канон (LOST), а не тихая порча. +function Build-GroupLevel { + param($fn) + $field = Get-Child $fn 'field' + $gt = Get-Child $fn 'groupType' + $pat = Get-Child $fn 'periodAdditionType' + $pabN = $fn.SelectSingleNode("dcsset:periodAdditionBegin", $ns) + $paeN = $fn.SelectSingleNode("dcsset:periodAdditionEnd", $ns) + $pab = $null; $pae = $null + if ($pabN) { $pt = $pabN.GetAttribute("type", $NS_XSI); $pv = $pabN.InnerText; if (($pt -match 'Field$') -or ($pv -and $pv -ne '0001-01-01T00:00:00')) { $pab = $pv } } + if ($paeN) { $pt = $paeN.GetAttribute("type", $NS_XSI); $pv = $paeN.InnerText; if (($pt -match 'Field$') -or ($pv -and $pv -ne '0001-01-01T00:00:00')) { $pae = $pv } } + $isDefault = ((-not $gt) -or $gt -eq 'Items') -and ((-not $pat) -or $pat -eq 'None') -and (-not $pab) -and (-not $pae) + if ($isDefault) { return $field } + $o = [ordered]@{ field = $field } + if ($gt -and $gt -ne 'Items') { $o['groupType'] = $gt } + if ($pat -and $pat -ne 'None') { $o['periodAdditionType'] = $pat } + if ($pab) { $o['periodAdditionBegin'] = $pab } + if ($pae) { $o['periodAdditionEnd'] = $pae } + return $o +} + +function Build-ListGrouping { + param($itemNode) + $levels = New-Object System.Collections.ArrayList + $cur = $itemNode + while ($cur) { + if (($cur.GetAttribute("type", $NS_XSI)) -notmatch 'StructureItemGroup$') { return $null } + $gi = $null; $nested = @() + foreach ($ch in $cur.ChildNodes) { + if ($ch.NodeType -ne [System.Xml.XmlNodeType]::Element) { continue } + switch ($ch.LocalName) { + 'groupItems' { if ($gi) { return $null }; $gi = $ch } + 'item' { $nested += $ch } + default { return $null } # use/name/filter/order/… — здесь не поддержано + } + } + if (-not $gi) { return $null } + $fieldItems = @($gi.SelectNodes("dcsset:item", $ns)) + if ($fieldItems.Count -ne 1) { return $null } + $fn = $fieldItems[0] + if (($fn.GetAttribute("type", $NS_XSI)) -notmatch 'GroupItemField$') { return $null } + [void]$levels.Add((Build-GroupLevel $fn)) + if ($nested.Count -eq 0) { break } + if ($nested.Count -gt 1) { return $null } # ветвление — не линейно + $cur = $nested[0] + } + if ($levels.Count -eq 0) { return $null } + return ,@($levels) +} + # --- 1b. Ring-3 scan: конструкции вне зоны поддержки (draft list) --- function Fail-Ring3 { param([string]$kind, [string]$loc) @@ -2562,7 +2617,12 @@ if ($attrsNode) { } # Форма скелета ListSettings: дескриптор только для НЕ-каноничных форм (частичные/минимальные). # Канон → $null (компилятор регенерит полный скелет, как раньше). - $lsShape = Get-ListSettingsShape $lsNode + # Группировка строк списка: прямой ListSettings (не в filter/order/CA). + $grpItemNode = $lsNode.SelectSingleNode("dcsset:item", $ns) + $grouping = $null + if ($grpItemNode) { $grouping = Build-ListGrouping $grpItemNode } + if ($null -ne $grouping) { $so['grouping'] = $grouping } + $lsShape = Get-ListSettingsShape $lsNode ($null -ne $grouping) if ($null -ne $lsShape) { $so['listSettings'] = $lsShape } } if ($so.Count -gt 0) { $ao['settings'] = $so } diff --git a/docs/form-dsl-spec.md b/docs/form-dsl-spec.md index 89f2ff72..088bee5f 100644 --- a/docs/form-dsl-spec.md +++ b/docs/form-dsl-spec.md @@ -946,6 +946,7 @@ Forgiving-синонимы типа: XML-имя (`SpreadSheetDocumentField`) и | `filter` | array | Отбор списка (грамматика как в СКД) | | `dataParameters` | array | Значения параметров запроса в настройках (``). **Грамматика как в СКД**: shorthand `"Имя = Значение @off @user"` или объект `{ parameter, value?, valueType?, use?, nilValue?, viewMode?, userSettingID?, userSettingPresentation? }`. В дин-списке частый паттерн — плейсхолдер отключённого параметра без значения: `"ИмяПараметра @off"` | | `conditionalAppearance` | array | Условное оформление списка (грамматика как в СКД) | +| `grouping` | string \| array | Группировка строк списка (см. ниже). Forgiving-синонимы: `structure`, `группировка` | `ManualQuery` выводится из наличия `query` — отдельным ключом не задаётся. @@ -983,6 +984,35 @@ Forgiving-синонимы типа: XML-имя (`SpreadSheetDocumentField`) и > **`value: null` при `valueListAllowed: true`** — явный маркер «эмитить ``». Платформа пишет nil-значение для valueListAllowed-параметра не всегда (корпус 27 с / 47 без); по умолчанию (ключ `value` отсутствует) компилятор его НЕ эмитит. Декомпилятор ставит `value: null`, когда оригинал содержит nil-тег. +#### grouping — группировка строк списка + +Уровни группировки над детальными записями списка (XML: цепочка `` в ``). Группировка списка — **линейная цепочка** (каждый уровень = одно поле; несколько уровней вкладываются друг в друга), поэтому DSL плоский: + +```json +"grouping": "Контрагент" // один уровень +"grouping": "Контрагент > Договор > Заказ" // вложенные уровни (внешний → внутренний) +"grouping": ["Контрагент", "Договор"] // то же массивом +``` + +Шорткат `>` разделяет уровни. Элемент уровня — строка (имя поля) ИЛИ объект для нестандартного поля (ключи = теги исходника): + +```json +"grouping": [ + "Контрагент", + { "field": "Период", "groupType": "Hierarchy" }, + { "field": "Дата", "periodAdditionType": "...", "periodAdditionBegin": "2024-01-01T00:00:00", "periodAdditionEnd": "Параметр.КонецПериода" } +] +``` + +| Ключ уровня | Значение | +|-------------|----------| +| `field` | Имя поля группировки | +| `groupType` | Тип группировки (умолчание `Items`; `Hierarchy` — с учётом иерархии) | +| `periodAdditionType` | Дополнение периода для группировки по дате (умолчание `None`) | +| `periodAdditionBegin` / `periodAdditionEnd` | Границы дополнения периода: ISO-дата (`xs:dateTime`) или путь к полю (`dcscor:Field`) — авто-детект | + +> Грамматика уровня совпадает с элементом `groupBy`/`groupFields` структуры СКД (см. [skd-dsl-spec.md](skd-dsl-spec.md)); отличие от СКД — плоская модель (нет `children`/`selection`/`order`/детальных записей, которых у группировки списка не бывает). + #### 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 c18ad92d..c265c257 100644 --- a/tests/skills/cases/form-compile/dynamic-list-form.json +++ b/tests/skills/cases/form-compile/dynamic-list-form.json @@ -20,6 +20,7 @@ { "name": "Список", "type": "DynamicList", "useAlways": ["~Артикул", "Список.Code", "Description"], "settings": { "mainTable": "Catalog.Товары", "dynamicDataRead": true, "autoSaveUserSettings": false, "parameters": [ { "name": "ВыборТовара", "type": "CatalogRef.Товары", "valueListAllowed": true, "value": null } ], + "grouping": "Description > Code", "order": [ "Description", "Code desc" ], "filter": [ "Артикул = _ @off @user", "Артикул like %тест%", "Description подобно %abc%", { "field": "Code", "op": "=", "userSettingID": "auto", "userSettingPresentation": "Код товара" }, 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 fd3beaeb..12c6746c 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 @@ -196,6 +196,28 @@ Normal UUID-007 + + + + Description + Items + None + 0001-01-01T00:00:00 + 0001-01-01T00:00:00 + + + + + + Code + Items + None + 0001-01-01T00:00:00 + 0001-01-01T00:00:00 + + + + Normal UUID-008