From d484a5b7ecb4a0c7a6a4f6292715f218025914bd Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Sat, 6 Jun 2026 20:40:01 +0300 Subject: [PATCH] =?UTF-8?q?feat(form-decompile,form-compile):=20=D0=B4?= =?UTF-8?q?=D0=BE=D1=81=D1=82=D1=83=D0=BF=20=D0=BF=D0=BE=20=D1=80=D0=BE?= =?UTF-8?q?=D0=BB=D1=8F=D0=BC=20=E2=80=94=20userVisible/view/edit/use=20(?= =?UTF-8?q?=D0=B5=D0=B4=D0=B8=D0=BD=D1=8B=D0=B9=20xr-=D0=BC=D0=B5=D1=85?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D0=B7=D0=BC)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Кластер «доступ по ролям»: единый role-adjustable boolean платформы (xr:Common + 0..N xr:Value name="Role.X") для четырёх владельцев одним грамматиком значения: - элемент → userVisible () - реквизит → view, edit (/) - команда → use () Значение DSL: скаляр false/true → голый ; объект { common, roles:{ Имя: bool } } → пер-ролевые исключения (три-state как в конфигураторе: роль не указана → наследует common; указана → явный bool). Имя роли forgiving: без префикса / Role. / Роль. → нормализуется в Role. Отсутствие ключа = полный доступ (платформа тег не пишет) — дефолт не эмитим. Декомпилятор инвертирует: голый Common → скаляр, есть Value → объект. Компилятор: общий хелпер Emit-XrFlag / emit_xr_flag (ps1+py). Порядок схемы: View → Edit после MainAttribute; Use после ToolTip до Action. Раньше: userVisible умел только голый false (компилятор), декомпилятор не захватывал ничего; view/edit/use не умел никто. TOTAL diff lines выборки 2.17: 7911 → 7560 (-351), match 9 → 11. Снапшоты attributes-types/commands сертифицированы в 1С (8.3.24); регресс form-compile 33/33 зелёный на ps + python. decompile v0.28, compile v1.47. Co-Authored-By: Claude Opus 4.8 --- .../form-compile/scripts/form-compile.ps1 | 43 ++++++++++++++++--- .../form-compile/scripts/form-compile.py | 42 +++++++++++++++--- .../form-decompile/scripts/form-decompile.ps1 | 29 ++++++++++++- docs/form-dsl-spec.md | 33 ++++++++++++++ .../cases/form-compile/attributes-types.json | 6 +-- tests/skills/cases/form-compile/commands.json | 2 +- .../Типы/Forms/Форма/Ext/Form.xml | 9 ++++ .../Команды/Forms/Форма/Ext/Form.xml | 3 ++ 8 files changed, 151 insertions(+), 16 deletions(-) diff --git a/.claude/skills/form-compile/scripts/form-compile.ps1 b/.claude/skills/form-compile/scripts/form-compile.ps1 index c2c7e8ca..384a6a52 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.46 — Compile 1C managed form from JSON or object metadata +# form-compile v1.47 — Compile 1C managed form from JSON or object metadata # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [string]$JsonPath, @@ -2430,14 +2430,39 @@ function Emit-Element { } } +# Role-adjustable boolean (xr:Common + 0..N xr:Value name="Role.X"). +# Единый механизм платформы: UserVisible (элементы), View/Edit (атрибуты), Use (команды/кнопки). +# Значение DSL: скаляр bool → только ; объект { common, roles:{ Имя: bool } } → +пер-ролевые исключения. +# Имя роли принимаем с/без префикса "Role." (forgiving); на выход всегда с префиксом. +function Emit-XrFlag { + param([string]$tag, $val, [string]$indent) + if ($null -eq $val) { return } + if ($val -is [bool]) { + X "$indent<$tag>" + X "$indent`t$(if ($val){'true'}else{'false'})" + X "$indent" + return + } + # объектная форма { common, roles } + $common = if ($null -ne $val.common) { [bool]$val.common } else { $false } + X "$indent<$tag>" + X "$indent`t$(if ($common){'true'}else{'false'})" + if ($val.roles) { + foreach ($r in $val.roles.PSObject.Properties) { + # Forgiving: принимаем имя без префикса, с "Role." или кириллическим "Роль." → нормализуем в "Role." + $rname = "$($r.Name)" -replace '^(Role|Роль)\.', '' + $rname = "Role.$rname" + $rval = if ([bool]$r.Value) { 'true' } else { 'false' } + X "$indent`t$rval" + } + } + X "$indent" +} + function Emit-CommonFlags { param($el, [string]$indent) if ($el.visible -eq $false -or $el.hidden -eq $true) { X "$indentfalse" } - if ($el.userVisible -eq $false) { - X "$indent" - X "$indent`tfalse" - X "$indent" - } + if ($null -ne $el.userVisible) { Emit-XrFlag -tag 'UserVisible' -val $el.userVisible -indent $indent } if ($el.enabled -eq $false -or $el.disabled -eq $true) { X "$indentfalse" } if ($el.readOnly -eq $true) { X "$indenttrue" } } @@ -3526,6 +3551,9 @@ function Emit-Attributes { if ($attr.main -eq $true) { X "$innertrue" } + # Доступ по ролям: просмотр/редактирование (порядок схемы: View → Edit, после MainAttribute) + if ($null -ne $attr.view) { Emit-XrFlag -tag 'View' -val $attr.view -indent $inner } + if ($null -ne $attr.edit) { Emit-XrFlag -tag 'Edit' -val $attr.edit -indent $inner } $mainSaved = $false if ($attr.main -eq $true -and $attr.type) { $mainSaved = ("$($attr.type)") -match '^(CatalogObject|DocumentObject|ChartOfAccountsObject|ChartOfCalculationTypesObject|ChartOfCharacteristicTypesObject|ExchangePlanObject|BusinessProcessObject|TaskObject)\.' -or ("$($attr.type)") -match 'RecordManager\.' @@ -3647,6 +3675,9 @@ function Emit-Commands { Emit-MLText -tag "ToolTip" -text $cmd.tooltip -indent $inner } + # Доступность команды по ролям (после ToolTip, до Action) + if ($null -ne $cmd.use) { Emit-XrFlag -tag 'Use' -val $cmd.use -indent $inner } + if ($cmd.action) { X "$inner$($cmd.action)" } diff --git a/.claude/skills/form-compile/scripts/form-compile.py b/.claude/skills/form-compile/scripts/form-compile.py index 3f552632..68274fc2 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.46 — Compile 1C managed form from JSON or object metadata +# form-compile v1.47 — Compile 1C managed form from JSON or object metadata # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import copy @@ -2059,13 +2059,36 @@ def emit_table_addition(lines, tag, table_name, name_suffix, src_type, indent): lines.append(f'{indent}') +# Role-adjustable boolean (xr:Common + 0..N xr:Value name="Role.X"). +# Единый механизм платформы: UserVisible (элементы), View/Edit (атрибуты), Use (команды/кнопки). +# Значение DSL: скаляр bool → только ; объект { common, roles:{ Имя: bool } } → +пер-ролевые исключения. +# Имя роли принимаем с/без префикса "Role." (forgiving); на выход всегда с префиксом. +def emit_xr_flag(lines, tag, val, indent): + if val is None: + return + if isinstance(val, bool): + lines.append(f"{indent}<{tag}>") + lines.append(f"{indent}\t{'true' if val else 'false'}") + lines.append(f"{indent}") + return + # объектная форма { common, roles } + common = bool(val.get('common')) if val.get('common') is not None else False + lines.append(f"{indent}<{tag}>") + lines.append(f"{indent}\t{'true' if common else 'false'}") + roles = val.get('roles') + if roles: + for rname, rval in roles.items(): + # Forgiving: имя без префикса, с "Role." или кириллическим "Роль." → нормализуем в "Role." + rn = "Role." + re.sub(r'^(Role|Роль)\.', '', rname) + lines.append(f"{indent}\t{'true' if rval else 'false'}") + lines.append(f"{indent}") + + def emit_common_flags(lines, el, indent): if el.get('visible') is False or el.get('hidden') is True: lines.append(f"{indent}false") - if el.get('userVisible') is False: - lines.append(f"{indent}") - lines.append(f"{indent}\tfalse") - lines.append(f"{indent}") + if el.get('userVisible') is not None: + emit_xr_flag(lines, 'UserVisible', el.get('userVisible'), indent) if el.get('enabled') is False or el.get('disabled') is True: lines.append(f"{indent}false") if el.get('readOnly') is True: @@ -3180,6 +3203,11 @@ def emit_attributes(lines, attrs, indent): if attr.get('main') is True: lines.append(f'{inner}true') + # Доступ по ролям: просмотр/редактирование (порядок схемы: View → Edit, после MainAttribute) + if attr.get('view') is not None: + emit_xr_flag(lines, 'View', attr.get('view'), inner) + if attr.get('edit') is not None: + emit_xr_flag(lines, 'Edit', attr.get('edit'), inner) main_saved = False if attr.get('main') is True and attr.get('type'): t = str(attr['type']) @@ -3285,6 +3313,10 @@ def emit_commands(lines, cmds, indent): if cmd.get('tooltip'): emit_mltext(lines, inner, 'ToolTip', cmd['tooltip']) + # Доступность команды по ролям (после ToolTip, до Action) + if cmd.get('use') is not None: + emit_xr_flag(lines, 'Use', cmd.get('use'), inner) + if cmd.get('action'): lines.append(f'{inner}{cmd["action"]}') diff --git a/.claude/skills/form-decompile/scripts/form-decompile.ps1 b/.claude/skills/form-decompile/scripts/form-decompile.ps1 index 2f14b3b7..c5dd842c 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.27 — Decompile 1C managed Form.xml to JSON DSL (draft) +# form-decompile v0.28 — Decompile 1C managed Form.xml to JSON DSL (draft) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # ВНИМАНИЕ: раундтрип не гарантируется. Навык исключён из авто-использования моделью. param( @@ -779,12 +779,36 @@ function Get-Events { return $events } +# Инверсия Emit-XrFlag: role-adjustable boolean (UserVisible/View/Edit/Use). +# […] → скаляр bool (без ролей) или объект { common, roles:{Имя:bool} }. +# Имя роли отдаём без префикса "Role.". Возвращает $null, если тег отсутствует. +function Decompile-XrFlag { + param($node, [string]$tag) + $el = $node.SelectSingleNode("*[local-name()='$tag']") + if (-not $el) { return $null } + $commonNode = $el.SelectSingleNode("*[local-name()='Common']") + $common = ($commonNode -and $commonNode.InnerText -eq 'true') + $valNodes = @($el.SelectNodes("*[local-name()='Value']")) + if ($valNodes.Count -eq 0) { return $common } + $roles = [ordered]@{} + foreach ($v in $valNodes) { + $rn = $v.GetAttribute("name") + if ($rn -match '^Role\.') { $rn = $rn.Substring(5) } + $roles[$rn] = ($v.InnerText -eq 'true') + } + $o = [ordered]@{} + $o['common'] = $common + $o['roles'] = $roles + return $o +} + # Общие свойства элемента (visible/enabled/readonly/title/events) → в hash function Add-CommonProps { param($obj, $node, [string]$elName) if ((Get-Child $node 'Visible') -eq 'false') { $obj['hidden'] = $true } if ((Get-Child $node 'Enabled') -eq 'false') { $obj['disabled'] = $true } if ((Get-Child $node 'ReadOnly') -eq 'true') { $obj['readOnly'] = $true } + $uv = Decompile-XrFlag $node 'UserVisible'; if ($null -ne $uv) { $obj['userVisible'] = $uv } $titleNode = $node.SelectSingleNode("lf:Title", $ns) if ($titleNode) { $t = Get-LangText $titleNode @@ -1186,6 +1210,8 @@ if ($attrsNode) { $ao['name'] = $a.GetAttribute("name") $ty = Decompile-Type ($a.SelectSingleNode("lf:Type", $ns)); if ($ty) { $ao['type'] = $ty } if ((Get-Child $a 'MainAttribute') -eq 'true') { $ao['main'] = $true } + $vw = Decompile-XrFlag $a 'View'; if ($null -ne $vw) { $ao['view'] = $vw } + $ed = Decompile-XrFlag $a 'Edit'; if ($null -ne $ed) { $ao['edit'] = $ed } $tNode = $a.SelectSingleNode("lf:Title", $ns); if ($tNode) { $t = Get-LangText $tNode; if ($null -ne $t) { $ao['title'] = $t } } if ((Get-Child $a 'SavedData') -eq 'true') { $ao['savedData'] = $true } $fc = Get-Child $a 'FillChecking'; if ($fc) { $ao['fillChecking'] = $fc } @@ -1279,6 +1305,7 @@ if ($cmdsNode) { $act = Get-Child $c 'Action'; if ($act) { $co['action'] = $act } $tNode = $c.SelectSingleNode("lf:Title", $ns); if ($tNode) { $t = Get-LangText $tNode; if ($null -ne $t) { $co['title'] = $t } } $ttNode = $c.SelectSingleNode("lf:ToolTip", $ns); if ($ttNode) { $t = Get-LangText $ttNode; if ($null -ne $t) { $co['tooltip'] = $t } } + $us = Decompile-XrFlag $c 'Use'; if ($null -ne $us) { $co['use'] = $us } $cru = Get-Child $c 'CurrentRowUse'; if ($cru) { $co['currentRowUse'] = $cru } $sc = Get-Child $c 'Shortcut'; if ($sc) { $co['shortcut'] = $sc } $ref = $c.SelectSingleNode("lf:Picture/xr:Ref", $ns); if ($ref) { $co['picture'] = $ref.InnerText } diff --git a/docs/form-dsl-spec.md b/docs/form-dsl-spec.md index daa92667..ddce04e7 100644 --- a/docs/form-dsl-spec.md +++ b/docs/form-dsl-spec.md @@ -114,6 +114,7 @@ | `hidden` | bool | `true` → `false` | | `disabled` | bool | `true` → `false` | | `readOnly` | bool | `true` → `true` | +| `userVisible` | bool/object | Пользовательская видимость по ролям (``). См. §4.1c. Отсутствие = виден всем | | `events` | object | Обработчики событий: `{ "ИмяСобытия": "ИмяОбработчика" }` — тот же формат, что у событий формы (§3). Значение `null` → имя по конвенции (§4.2). См. §4.2 | | `titleLocation` | string | Расположение заголовка: `none`/`left`/`right`/`top`/`bottom`/`auto`. Эмитится при наличии (input, labelField, picField, table, calendar). У `check`/`radio` — особая семантика с умным дефолтом (см. их разделы) | | `tooltip` | string/object | Всплывающая подсказка элемента (``). Строка → ru, объект `{ "ru": …, "en": … }` → мультиязычный (как `title`). Эмитится сразу после `title` | @@ -131,6 +132,35 @@ Флаг авто-детектится по наличию известной разметки/``: для plain-строки объект не нужен. Явная форма `{text, formatted}` — только когда авто-детект неверен (formatted-текст без разметки, либо буквальные `<…>`-плейсхолдеры в неформатированном). +### 4.1c. Доступ по ролям (`userVisible` / `view` / `edit` / `use`) + +Единый механизм платформы (role-adjustable boolean): «общее значение + исключения по ролям». +Один и тот же грамматик-значения у разных ключей на разных владельцах: + +| Ключ | Владелец | XML-тег | Смысл | +|------|----------|---------|-------| +| `userVisible` | элемент (§4.1) | `` | пользовательская видимость | +| `view` | реквизит (§5) | `` | просмотр | +| `edit` | реквизит (§5) | `` | редактирование | +| `use` | команда (§7) | `` | доступность команды | + +**Значение** (общее для всех четырёх): +- скаляр `false`/`true` → только ``, без ролей (массовый случай, особенно `userVisible: false`); +- объект `{ "common": , "roles": { "ИмяРоли": , … } }` → `` + по `` на каждое исключение. + +Семантика как в конфигураторе (три состояния флага роли): роль, **не указанная** в `roles`, наследует `common`; указанная — задаёт явный `true`/`false` (может совпадать с `common`). + +**Имя роли** — forgiving: принимается без префикса (`ПолныеПрава`), с `Role.` или кириллическим `Роль.`; нормализуется в `Role.ИмяРоли`. + +**Отсутствие ключа** = полный доступ (платформа тег не пишет) — дефолт не эмитим. + +```jsonc +{ "inputField": "Поле", "userVisible": false } // скрыт у всех +{ "name": "Реквизит", "view": false, // не виден… + "edit": { "common": false, "roles": { "ПолныеПрава": true } } } // …и редактируем только Полными правами +{ "name": "Команда", "use": { "common": false, "roles": { "Роль.Бухгалтер": true } } } +``` + ### 4.1a. Общие layout-свойства Применимы к любому элементу (размеры, растягивание, выравнивание внутри родителя). Эмитятся только при указании. @@ -542,6 +572,8 @@ Pages поддерживает `pagesRepresentation`: `None`, `TabsOnTop`, `Tabs | `type` | string | Тип (shorthand) | | `main` | bool | Основной реквизит формы | | `title` | string | Заголовок | +| `view` | bool/object | Просмотр по ролям (``). См. §4.1c | +| `edit` | bool/object | Редактирование по ролям (``). См. §4.1c | | `savedData` | bool | Сохраняемые данные | | `fillChecking` | string | `Show`, `DontShow` | | `columns` | array | Колонки для ValueTable/ValueTree | @@ -630,6 +662,7 @@ Pages поддерживает `pagesRepresentation`: `None`, `TabsOnTop`, `Tabs | `action` | string | Имя процедуры-обработчика | | `title` | string | Заголовок | | `tooltip` | string/object | Всплывающая подсказка команды (``) | +| `use` | bool/object | Доступность команды по ролям (``). См. §4.1c | | `currentRowUse` | string | Использование текущей строки: `Auto`, `DontUse`, `Use` | | `shortcut` | string | Клавиатурное сочетание | | `picture` | string | Ссылка на картинку | diff --git a/tests/skills/cases/form-compile/attributes-types.json b/tests/skills/cases/form-compile/attributes-types.json index f32bb3e4..6a4e5ce5 100644 --- a/tests/skills/cases/form-compile/attributes-types.json +++ b/tests/skills/cases/form-compile/attributes-types.json @@ -17,14 +17,14 @@ "title": "Разные типы", "elements": [ { "input": "Строка", "path": "Строка" }, - { "input": "Число", "path": "Число" }, + { "input": "Число", "path": "Число", "userVisible": false }, { "input": "Дата", "path": "Дата" }, { "input": "Булево", "path": "Булево" } ], "attributes": [ { "name": "Объект", "type": "DataProcessorObject.Типы", "main": true }, - { "name": "Строка", "type": "string(200)" }, - { "name": "Число", "type": "decimal(10,0,nonneg)" }, + { "name": "Строка", "type": "string(200)", "view": false }, + { "name": "Число", "type": "decimal(10,0,nonneg)", "edit": false }, { "name": "Дата", "type": "dateTime" }, { "name": "Булево", "type": "boolean" } ] diff --git a/tests/skills/cases/form-compile/commands.json b/tests/skills/cases/form-compile/commands.json index 6cef4705..725f7530 100644 --- a/tests/skills/cases/form-compile/commands.json +++ b/tests/skills/cases/form-compile/commands.json @@ -27,7 +27,7 @@ { "name": "Результат", "type": "string" } ], "commands": [ - { "name": "Выполнить", "action": "ВыполнитьОбработка", "shortcut": "Ctrl+Enter" } + { "name": "Выполнить", "action": "ВыполнитьОбработка", "shortcut": "Ctrl+Enter", "use": false } ] } } diff --git a/tests/skills/cases/form-compile/snapshots/attributes-types/DataProcessors/Типы/Forms/Форма/Ext/Form.xml b/tests/skills/cases/form-compile/snapshots/attributes-types/DataProcessors/Типы/Forms/Форма/Ext/Form.xml index 2916c692..94420237 100644 --- a/tests/skills/cases/form-compile/snapshots/attributes-types/DataProcessors/Типы/Forms/Форма/Ext/Form.xml +++ b/tests/skills/cases/form-compile/snapshots/attributes-types/DataProcessors/Типы/Forms/Форма/Ext/Form.xml @@ -16,6 +16,9 @@ Число + + false + @@ -51,6 +54,9 @@ Variable + + false + @@ -67,6 +73,9 @@ <v8:AllowedSign>Nonnegative</v8:AllowedSign> </v8:NumberQualifiers> </Type> + <Edit> + <xr:Common>false</xr:Common> + </Edit> </Attribute> <Attribute name="Дата" id="16"> <Title> diff --git a/tests/skills/cases/form-compile/snapshots/commands/DataProcessors/Команды/Forms/Форма/Ext/Form.xml b/tests/skills/cases/form-compile/snapshots/commands/DataProcessors/Команды/Forms/Форма/Ext/Form.xml index a9b5f3f4..19659c8c 100644 --- a/tests/skills/cases/form-compile/snapshots/commands/DataProcessors/Команды/Forms/Форма/Ext/Form.xml +++ b/tests/skills/cases/form-compile/snapshots/commands/DataProcessors/Команды/Forms/Форма/Ext/Form.xml @@ -68,6 +68,9 @@ <v8:content>Выполнить</v8:content> </v8:item> + + false + ВыполнитьОбработка Ctrl+Enter