Add HTML5 access profile workspace
This commit is contained in:
@@ -45,6 +45,7 @@ def _topbar(project_id: str, project_nav: str) -> str:
|
|||||||
<nav class="project-nav">{project_nav}</nav>
|
<nav class="project-nav">{project_nav}</nav>
|
||||||
<a class="button" href="/docs">API</a>
|
<a class="button" href="/docs">API</a>
|
||||||
<a class="button" href="/html5/operations">Операции</a>
|
<a class="button" href="/html5/operations">Операции</a>
|
||||||
|
<a class="button" href="/html5/projects/{quote(project_id)}/access">Права</a>
|
||||||
<a class="button" href="/html5/projects/{quote(project_id)}/setup">HTML5 Setup</a>
|
<a class="button" href="/html5/projects/{quote(project_id)}/setup">HTML5 Setup</a>
|
||||||
<a class="button" href="/editor?project={quote(project_id)}">Legacy Next</a>
|
<a class="button" href="/editor?project={quote(project_id)}">Legacy Next</a>
|
||||||
</header>"""
|
</header>"""
|
||||||
|
|||||||
@@ -0,0 +1,263 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from html import escape
|
||||||
|
from typing import Iterable
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
|
from api_server.html5 import _metric, _page, _project_link, _topbar
|
||||||
|
|
||||||
|
|
||||||
|
def render_html5_access_page(
|
||||||
|
*,
|
||||||
|
project_id: str,
|
||||||
|
projects: Iterable[object],
|
||||||
|
normalized: object | None,
|
||||||
|
selected_profile: str | None = None,
|
||||||
|
plan: object | None = None,
|
||||||
|
dry_run: object | None = None,
|
||||||
|
error: str | None = None,
|
||||||
|
) -> str:
|
||||||
|
project_nav = "\n".join(_project_link(project, project_id) for project in projects)
|
||||||
|
if error or normalized is None:
|
||||||
|
content = f"""
|
||||||
|
<main class="workspace" data-html5-page="access" data-project-id="{escape(project_id)}">
|
||||||
|
{_topbar(project_id, project_nav)}
|
||||||
|
<section class="empty-state" data-html5-error>
|
||||||
|
<h1>Права доступа не загружены</h1>
|
||||||
|
<p>{escape(error or "NormalizedProject не найден")}</p>
|
||||||
|
<a class="button" href="/html5/projects/{quote(project_id)}/setup">Открыть setup</a>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
"""
|
||||||
|
return _page(f"SFERA Access - {project_id}", content)
|
||||||
|
|
||||||
|
access = getattr(normalized, "access", None)
|
||||||
|
profiles = list(getattr(access, "profiles", []) or [])
|
||||||
|
groups = list(getattr(access, "groups", []) or [])
|
||||||
|
users = list(getattr(access, "users", []) or [])
|
||||||
|
selected = _selected_profile(profiles, selected_profile)
|
||||||
|
plan_html = render_html5_access_publish_plan(project_id=project_id, profile=selected, plan=plan)
|
||||||
|
dry_run_html = render_html5_access_publish_result(project_id=project_id, result=dry_run)
|
||||||
|
content = f"""
|
||||||
|
<main class="workspace access-workspace" data-html5-page="access" data-project-id="{escape(project_id)}">
|
||||||
|
{_topbar(project_id, project_nav)}
|
||||||
|
<section class="access-layout">
|
||||||
|
<aside class="panel access-nav" data-html5-access-nav>
|
||||||
|
<div class="panel-title">Профили доступа</div>
|
||||||
|
<dl class="metrics">
|
||||||
|
{_metric("Профили", len(profiles))}
|
||||||
|
{_metric("Группы", len(groups))}
|
||||||
|
{_metric("Пользователи", len(users))}
|
||||||
|
{_metric("Назначения", _assignment_count(profiles, groups, users))}
|
||||||
|
</dl>
|
||||||
|
<nav>{''.join(_profile_link(project_id, item, selected) for item in profiles) or '<p class="muted padded">Профили не найдены</p>'}</nav>
|
||||||
|
</aside>
|
||||||
|
<section class="editor access-main" data-html5-access-main>
|
||||||
|
<div class="editor-head">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow">1C access model</p>
|
||||||
|
<h1>{escape(_profile_name(selected) if selected is not None else "Права доступа")}</h1>
|
||||||
|
</div>
|
||||||
|
<a class="button" href="/html5/projects/{quote(project_id)}/editor">Редактор</a>
|
||||||
|
</div>
|
||||||
|
{render_html5_access_profile(project_id=project_id, profile=selected)}
|
||||||
|
<div data-html5-access-plan>{plan_html}</div>
|
||||||
|
<div data-html5-access-result>{dry_run_html}</div>
|
||||||
|
</section>
|
||||||
|
<aside class="panel access-side" data-html5-access-side>
|
||||||
|
<div class="panel-title">Группы доступа</div>
|
||||||
|
<div class="access-list">{''.join(_group_card(item) for item in groups[:80]) or '<p class="muted padded">Группы не найдены</p>'}</div>
|
||||||
|
<div class="panel-title">Пользователи</div>
|
||||||
|
<div class="access-list">{''.join(_user_card(item) for item in users[:80]) or '<p class="muted padded">Пользователи не найдены</p>'}</div>
|
||||||
|
</aside>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
"""
|
||||||
|
return _page(f"SFERA Access - {project_id}", content)
|
||||||
|
|
||||||
|
|
||||||
|
def render_html5_access_profile(*, project_id: str, profile: object | None) -> str:
|
||||||
|
if profile is None:
|
||||||
|
return """
|
||||||
|
<section class="access-empty">
|
||||||
|
<strong>Выберите профиль доступа</strong>
|
||||||
|
<span>План публикации и dry-run будут построены сервером по данным нормализованного объекта 1С.</span>
|
||||||
|
</section>
|
||||||
|
"""
|
||||||
|
roles = list(getattr(profile, "roles", []) or [])
|
||||||
|
attrs = dict(getattr(profile, "attributes", {}) or {})
|
||||||
|
target_objects = list(attrs.get("target_objects") or [])
|
||||||
|
permissions = list(attrs.get("permissions") or [])
|
||||||
|
profile_name = _profile_name(profile)
|
||||||
|
return f"""
|
||||||
|
<section class="access-profile" data-html5-access-profile="{escape(profile_name)}">
|
||||||
|
<div class="source-head">
|
||||||
|
<div>
|
||||||
|
<strong>{escape(profile_name)}</strong>
|
||||||
|
<small>{escape(str(getattr(profile, "qualified_name", "") or ""))}</small>
|
||||||
|
</div>
|
||||||
|
<dl>
|
||||||
|
{_metric("Роли", len(roles))}
|
||||||
|
{_metric("Объекты", len(target_objects))}
|
||||||
|
{_metric("Права", len(permissions))}
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
<div class="access-summary">
|
||||||
|
<span>{escape(str(getattr(profile, "source", "") or "workspace"))}</span>
|
||||||
|
<span>{escape(str(attrs.get("status") or "loaded"))}</span>
|
||||||
|
</div>
|
||||||
|
<div class="access-role-grid">
|
||||||
|
{''.join(_role_card(role) for role in roles) or '<p class="muted padded">Роли не назначены</p>'}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def render_html5_access_publish_plan(*, project_id: str, profile: object | None, plan: object | None) -> str:
|
||||||
|
if profile is None:
|
||||||
|
return '<section class="access-plan"><div class="panel-title">План публикации</div><p class="muted padded">Нет выбранного профиля</p></section>'
|
||||||
|
profile_name = _profile_name(profile)
|
||||||
|
if plan is None:
|
||||||
|
return f"""
|
||||||
|
<section class="access-plan">
|
||||||
|
<div class="panel-title">План публикации</div>
|
||||||
|
<div class="access-actions">
|
||||||
|
<button
|
||||||
|
hx-get="/html5/projects/{quote(project_id)}/access/profiles/{quote(profile_name, safe='')}/plan"
|
||||||
|
hx-target="[data-html5-access-plan]"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
>Построить план</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
"""
|
||||||
|
operations = list(getattr(plan, "operations", []) or [])
|
||||||
|
warnings = list(getattr(plan, "warnings", []) or [])
|
||||||
|
ready = bool(getattr(plan, "ready_for_extension", False))
|
||||||
|
warning_html = "".join(f"<li>{escape(str(item))}</li>" for item in warnings)
|
||||||
|
dry_run_button = (
|
||||||
|
f"""
|
||||||
|
<form
|
||||||
|
hx-post="/html5/projects/{quote(project_id)}/access/profiles/{quote(profile_name, safe='')}/publish-dry-run"
|
||||||
|
hx-target="[data-html5-access-result]"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
>
|
||||||
|
<button type="submit" class="primary">Dry-run в 1С</button>
|
||||||
|
</form>
|
||||||
|
"""
|
||||||
|
if ready
|
||||||
|
else '<p class="muted padded">План не готов к отправке в расширение</p>'
|
||||||
|
)
|
||||||
|
return f"""
|
||||||
|
<section class="access-plan">
|
||||||
|
<div class="panel-title">План публикации</div>
|
||||||
|
<div class="access-plan-head">
|
||||||
|
<span class="status-pill">{'готов' if ready else 'требует проверки'}</span>
|
||||||
|
<strong>{len(operations)} операций</strong>
|
||||||
|
{dry_run_button}
|
||||||
|
</div>
|
||||||
|
<ul class="access-warnings">{warning_html}</ul>
|
||||||
|
<div class="access-operations">{''.join(_operation_card(item) for item in operations) or '<p class="muted padded">Операций нет</p>'}</div>
|
||||||
|
</section>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def render_html5_access_publish_result(*, project_id: str, result: object | None) -> str:
|
||||||
|
if result is None:
|
||||||
|
return '<section class="access-result"><div class="panel-title">Ответ расширения</div><p class="muted padded">Dry-run еще не выполнялся</p></section>'
|
||||||
|
checks = list(getattr(result, "checks", []) or [])
|
||||||
|
payload = dict(getattr(result, "result", {}) or {})
|
||||||
|
status = str(getattr(result, "status", ""))
|
||||||
|
ready = bool(getattr(result, "ready", False))
|
||||||
|
return f"""
|
||||||
|
<section class="access-result" data-html5-access-result-status="{escape(status)}">
|
||||||
|
<div class="panel-title">Ответ расширения</div>
|
||||||
|
<div class="access-plan-head">
|
||||||
|
<span class="status-pill">{escape(status)}</span>
|
||||||
|
<strong>{'расширение ответило' if ready else 'требуется настройка публикации'}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="access-operations">{''.join(_check_card(item) for item in checks)}</div>
|
||||||
|
<pre class="access-json">{escape(_short_json(payload))}</pre>
|
||||||
|
</section>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def _selected_profile(profiles: list[object], selected_profile: str | None) -> object | None:
|
||||||
|
if not profiles:
|
||||||
|
return None
|
||||||
|
if not selected_profile:
|
||||||
|
return profiles[0]
|
||||||
|
wanted = selected_profile.casefold()
|
||||||
|
return next(
|
||||||
|
(
|
||||||
|
item
|
||||||
|
for item in profiles
|
||||||
|
if _profile_name(item).casefold() == wanted
|
||||||
|
or str(getattr(item, "qualified_name", "") or "").casefold() == wanted
|
||||||
|
),
|
||||||
|
profiles[0],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _profile_link(project_id: str, profile: object, selected: object | None) -> str:
|
||||||
|
name = _profile_name(profile)
|
||||||
|
active = selected is profile
|
||||||
|
roles = list(getattr(profile, "roles", []) or [])
|
||||||
|
return f"""
|
||||||
|
<a class="tree-item" data-html5-access-profile-selected="{str(active).lower()}" href="/html5/projects/{quote(project_id)}/access?profile={quote(name)}">
|
||||||
|
<span>{escape(name)}</span>
|
||||||
|
<small>{len(roles)} ролей</small>
|
||||||
|
</a>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def _profile_name(profile: object) -> str:
|
||||||
|
return str(getattr(profile, "name", None) or getattr(profile, "qualified_name", None) or "Профиль")
|
||||||
|
|
||||||
|
|
||||||
|
def _role_card(role: object) -> str:
|
||||||
|
name = str(getattr(role, "role_qualified_name", None) or getattr(role, "role", None) or role)
|
||||||
|
source = str(getattr(role, "source", "") or "")
|
||||||
|
return f'<article class="access-card"><strong>{escape(name)}</strong><small>{escape(source)}</small></article>'
|
||||||
|
|
||||||
|
|
||||||
|
def _group_card(group: object) -> str:
|
||||||
|
name = str(getattr(group, "name", ""))
|
||||||
|
profile = str(getattr(group, "profile_qualified_name", None) or getattr(group, "profile", None) or "без профиля")
|
||||||
|
users = list(getattr(group, "users", []) or [])
|
||||||
|
return f'<article class="access-card"><strong>{escape(name)}</strong><small>{escape(profile)} · {len(users)} пользователей</small></article>'
|
||||||
|
|
||||||
|
|
||||||
|
def _user_card(user: object) -> str:
|
||||||
|
name = str(getattr(user, "name", ""))
|
||||||
|
full_name = str(getattr(user, "full_name", "") or "")
|
||||||
|
groups = list(getattr(user, "groups", []) or [])
|
||||||
|
return f'<article class="access-card"><strong>{escape(name)}</strong><small>{escape(full_name)} · {len(groups)} групп</small></article>'
|
||||||
|
|
||||||
|
|
||||||
|
def _operation_card(operation: dict) -> str:
|
||||||
|
action = str(operation.get("action", "operation"))
|
||||||
|
target = str(operation.get("target", ""))
|
||||||
|
detail = str(operation.get("role") or operation.get("profile") or operation.get("name") or "")
|
||||||
|
return f'<article class="access-card"><strong>{escape(action)}</strong><small>{escape(target)} {escape(detail)}</small></article>'
|
||||||
|
|
||||||
|
|
||||||
|
def _check_card(check: object) -> str:
|
||||||
|
code = str(getattr(check, "code", ""))
|
||||||
|
status = str(getattr(check, "status", ""))
|
||||||
|
message = str(getattr(check, "message", ""))
|
||||||
|
return f'<article class="access-card"><strong>{escape(code)} · {escape(status)}</strong><small>{escape(message)}</small></article>'
|
||||||
|
|
||||||
|
|
||||||
|
def _assignment_count(profiles: list[object], groups: list[object], users: list[object]) -> int:
|
||||||
|
return (
|
||||||
|
sum(len(getattr(item, "roles", []) or []) for item in profiles)
|
||||||
|
+ sum(len(getattr(item, "roles", []) or []) + len(getattr(item, "users", []) or []) for item in groups)
|
||||||
|
+ sum(len(getattr(item, "roles", []) or []) + len(getattr(item, "groups", []) or []) for item in users)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _short_json(payload: dict) -> str:
|
||||||
|
if not payload:
|
||||||
|
return "{}"
|
||||||
|
items = [f"{key}: {value}" for key, value in list(payload.items())[:12]]
|
||||||
|
return "\n".join(items)
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Awaitable, Callable, Iterable
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from fastapi import HTTPException
|
||||||
|
|
||||||
|
from api_server.html5_access import (
|
||||||
|
render_html5_access_page,
|
||||||
|
render_html5_access_publish_plan,
|
||||||
|
render_html5_access_publish_result,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def html5_access_page(
|
||||||
|
*,
|
||||||
|
project_id: str,
|
||||||
|
profile: str | None,
|
||||||
|
project_summaries: Callable[[], Iterable[object]],
|
||||||
|
normalized_project: Callable[[str], object],
|
||||||
|
access_profile_by_name: Callable[[object, str], object | None],
|
||||||
|
access_publish_plan: Callable[[object, object], object],
|
||||||
|
) -> str:
|
||||||
|
try:
|
||||||
|
normalized = normalized_project(project_id)
|
||||||
|
selected = _selected_profile(normalized, profile, access_profile_by_name)
|
||||||
|
plan = access_publish_plan(normalized, selected) if selected is not None else None
|
||||||
|
return render_html5_access_page(
|
||||||
|
project_id=project_id,
|
||||||
|
projects=project_summaries(),
|
||||||
|
normalized=normalized,
|
||||||
|
selected_profile=profile,
|
||||||
|
plan=plan,
|
||||||
|
)
|
||||||
|
except HTTPException as error:
|
||||||
|
return render_html5_access_page(
|
||||||
|
project_id=project_id,
|
||||||
|
projects=project_summaries(),
|
||||||
|
normalized=None,
|
||||||
|
error=str(error.detail),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def html5_access_publish_plan(
|
||||||
|
*,
|
||||||
|
project_id: str,
|
||||||
|
profile_name: str,
|
||||||
|
normalized_project: Callable[[str], object],
|
||||||
|
access_profile_by_name: Callable[[object, str], object | None],
|
||||||
|
access_publish_plan: Callable[[object, object], object],
|
||||||
|
) -> str:
|
||||||
|
normalized = normalized_project(project_id)
|
||||||
|
profile = access_profile_by_name(normalized, profile_name)
|
||||||
|
if profile is None:
|
||||||
|
raise HTTPException(status_code=404, detail="Access profile not found")
|
||||||
|
return render_html5_access_publish_plan(
|
||||||
|
project_id=project_id,
|
||||||
|
profile=profile,
|
||||||
|
plan=access_publish_plan(normalized, profile),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def html5_access_publish_dry_run(
|
||||||
|
*,
|
||||||
|
project_id: str,
|
||||||
|
profile_name: str,
|
||||||
|
publish_dry_run: Callable[[str, str], Awaitable[Any]],
|
||||||
|
) -> str:
|
||||||
|
result = await publish_dry_run(project_id, profile_name)
|
||||||
|
return render_html5_access_publish_result(project_id=project_id, result=result)
|
||||||
|
|
||||||
|
|
||||||
|
def _selected_profile(
|
||||||
|
normalized: object,
|
||||||
|
profile_name: str | None,
|
||||||
|
access_profile_by_name: Callable[[object, str], object | None],
|
||||||
|
) -> object | None:
|
||||||
|
access = getattr(normalized, "access", None)
|
||||||
|
profiles = list(getattr(access, "profiles", []) or [])
|
||||||
|
if not profiles:
|
||||||
|
return None
|
||||||
|
if not profile_name:
|
||||||
|
return profiles[0]
|
||||||
|
return access_profile_by_name(normalized, profile_name) or profiles[0]
|
||||||
@@ -55,6 +55,11 @@ from api_server.agent_models import (
|
|||||||
from api_server.html5_forms import (
|
from api_server.html5_forms import (
|
||||||
html5_form_data as _html5_form_data,
|
html5_form_data as _html5_form_data,
|
||||||
)
|
)
|
||||||
|
from api_server.html5_access_controller import (
|
||||||
|
html5_access_page as _html5_access_page,
|
||||||
|
html5_access_publish_dry_run as _html5_access_publish_dry_run,
|
||||||
|
html5_access_publish_plan as _html5_access_publish_plan,
|
||||||
|
)
|
||||||
from api_server.html5_editor_controller import (
|
from api_server.html5_editor_controller import (
|
||||||
html5_editor_page as _html5_editor_page,
|
html5_editor_page as _html5_editor_page,
|
||||||
html5_form_editor_page as _html5_form_editor_page,
|
html5_form_editor_page as _html5_form_editor_page,
|
||||||
@@ -1532,6 +1537,44 @@ async def html5_project_form_editor(project_id: str, form: str | None = None) ->
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/html5/projects/{project_id}/access")
|
||||||
|
async def html5_project_access(project_id: str, profile: str | None = None) -> Response:
|
||||||
|
return _html5_response(
|
||||||
|
_html5_access_page(
|
||||||
|
project_id=project_id,
|
||||||
|
profile=profile,
|
||||||
|
project_summaries=_project_summaries,
|
||||||
|
normalized_project=_normalized_project_or_404,
|
||||||
|
access_profile_by_name=_access_profile_by_name,
|
||||||
|
access_publish_plan=_build_access_profile_publish_plan,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/html5/projects/{project_id}/access/profiles/{profile_name}/plan")
|
||||||
|
async def html5_project_access_profile_plan(project_id: str, profile_name: str) -> Response:
|
||||||
|
return _html5_response(
|
||||||
|
_html5_access_publish_plan(
|
||||||
|
project_id=project_id,
|
||||||
|
profile_name=profile_name,
|
||||||
|
normalized_project=_normalized_project_or_404,
|
||||||
|
access_profile_by_name=_access_profile_by_name,
|
||||||
|
access_publish_plan=_build_access_profile_publish_plan,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/html5/projects/{project_id}/access/profiles/{profile_name}/publish-dry-run")
|
||||||
|
async def html5_project_access_profile_publish_dry_run(project_id: str, profile_name: str) -> Response:
|
||||||
|
return _html5_response(
|
||||||
|
await _html5_access_publish_dry_run(
|
||||||
|
project_id=project_id,
|
||||||
|
profile_name=profile_name,
|
||||||
|
publish_dry_run=dry_run_project_access_profile_publish,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.post("/html5/projects/{project_id}/forms/editor/preview")
|
@app.post("/html5/projects/{project_id}/forms/editor/preview")
|
||||||
async def html5_project_form_editor_preview(project_id: str, request: Request) -> Response:
|
async def html5_project_form_editor_preview(project_id: str, request: Request) -> Response:
|
||||||
form = await _html5_form_data(request)
|
form = await _html5_form_data(request)
|
||||||
@@ -2927,6 +2970,13 @@ async def get_normalized_project(project_id: str) -> NormalizedProject:
|
|||||||
return normalized
|
return normalized
|
||||||
|
|
||||||
|
|
||||||
|
def _normalized_project_or_404(project_id: str) -> NormalizedProject:
|
||||||
|
normalized = _load_normalized_project(project_id)
|
||||||
|
if normalized is None:
|
||||||
|
raise HTTPException(status_code=404, detail="NormalizedProject not found")
|
||||||
|
return normalized
|
||||||
|
|
||||||
|
|
||||||
@app.get("/projects/{project_id}/normalized/summary", response_model=NormalizedProjectSummary)
|
@app.get("/projects/{project_id}/normalized/summary", response_model=NormalizedProjectSummary)
|
||||||
async def get_normalized_project_summary(project_id: str) -> NormalizedProjectSummary:
|
async def get_normalized_project_summary(project_id: str) -> NormalizedProjectSummary:
|
||||||
normalized = _load_normalized_project(project_id)
|
normalized = _load_normalized_project(project_id)
|
||||||
|
|||||||
@@ -10,6 +10,8 @@
|
|||||||
.form-editor-layout{grid-template-columns:280px minmax(0,1.2fr) minmax(320px,.8fr)}.form-editor-actions{display:flex;gap:8px;flex-wrap:wrap}.tree-item[data-html5-form-selected="true"]{background:#f8fbff;border-left-color:var(--brand)}.form-designer{height:calc(100% - 72px);display:grid;grid-template-rows:auto minmax(0,1fr) auto;overflow:hidden;background:#f7f9fc}.form-designer-head,.form-designer-foot{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 14px;border-bottom:1px solid var(--line);background:#fff}.form-designer-head strong,.form-designer-head small{display:block}.form-designer-head small{color:var(--muted)}.form-designer-head span{border:1px solid var(--line);background:#eef6f1;color:var(--ok);padding:5px 8px;font-size:12px;font-weight:900}.form-designer-foot{border-top:1px solid var(--line);border-bottom:0}.form-canvas{min-height:0;overflow:auto;padding:18px}.form-window{max-width:720px;min-height:420px;margin:0 auto;border:1px solid var(--line);background:#fff;box-shadow:0 8px 24px rgba(15,23,42,.08)}.form-window-title{height:38px;display:flex;align-items:center;border-bottom:1px solid var(--line);padding:0 12px;font-weight:900;background:#fbfcfe}.form-command-bar{display:flex;gap:6px;flex-wrap:wrap;padding:10px 12px;border-bottom:1px solid var(--line)}.form-command-bar button{height:28px;font-size:12px}.form-fields{display:grid;gap:10px;padding:14px}.form-field{display:grid;grid-template-columns:160px minmax(0,1fr);gap:8px;align-items:center}.form-field span{font-size:12px;font-weight:800;color:var(--muted)}.form-field input{height:30px;border:1px solid var(--line);padding:0 8px;background:#fbfaf7}
|
.form-editor-layout{grid-template-columns:280px minmax(0,1.2fr) minmax(320px,.8fr)}.form-editor-actions{display:flex;gap:8px;flex-wrap:wrap}.tree-item[data-html5-form-selected="true"]{background:#f8fbff;border-left-color:var(--brand)}.form-designer{height:calc(100% - 72px);display:grid;grid-template-rows:auto minmax(0,1fr) auto;overflow:hidden;background:#f7f9fc}.form-designer-head,.form-designer-foot{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 14px;border-bottom:1px solid var(--line);background:#fff}.form-designer-head strong,.form-designer-head small{display:block}.form-designer-head small{color:var(--muted)}.form-designer-head span{border:1px solid var(--line);background:#eef6f1;color:var(--ok);padding:5px 8px;font-size:12px;font-weight:900}.form-designer-foot{border-top:1px solid var(--line);border-bottom:0}.form-canvas{min-height:0;overflow:auto;padding:18px}.form-window{max-width:720px;min-height:420px;margin:0 auto;border:1px solid var(--line);background:#fff;box-shadow:0 8px 24px rgba(15,23,42,.08)}.form-window-title{height:38px;display:flex;align-items:center;border-bottom:1px solid var(--line);padding:0 12px;font-weight:900;background:#fbfcfe}.form-command-bar{display:flex;gap:6px;flex-wrap:wrap;padding:10px 12px;border-bottom:1px solid var(--line)}.form-command-bar button{height:28px;font-size:12px}.form-fields{display:grid;gap:10px;padding:14px}.form-field{display:grid;grid-template-columns:160px minmax(0,1fr);gap:8px;align-items:center}.form-field span{font-size:12px;font-weight:800;color:var(--muted)}.form-field input{height:30px;border:1px solid var(--line);padding:0 8px;background:#fbfaf7}
|
||||||
.form-designer-body{min-height:0;display:grid;grid-template-columns:minmax(0,1fr) 340px;overflow:hidden}.form-property-panel{min-height:0;overflow:auto;border-left:1px solid var(--line);background:#fff}.property-row{display:grid;grid-template-columns:1fr 132px;gap:8px;padding:12px;border-bottom:1px solid var(--line)}.property-row label,.form-editor-row label{display:grid;gap:4px;font-size:11px;font-weight:900;color:var(--muted);text-transform:uppercase}.property-row input,.property-row select,.form-editor-row input,.form-editor-row select,.form-add-row input,.form-add-row select{height:30px;min-width:0;border:1px solid var(--line);background:#fff;padding:0 8px;color:var(--text);font:13px/1.2 system-ui,-apple-system,Segoe UI,sans-serif;text-transform:none}.form-window{border-color:#b9c1cd;background:#fdfdfd;box-shadow:0 12px 28px rgba(33,43,54,.12)}.form-window-title{justify-content:space-between;height:34px;background:linear-gradient(#f8f9fb,#e9edf3);border-bottom-color:#cbd3df;color:#1f2937}.form-window-title small{font-size:11px;color:#687385;font-weight:800}.form-command-bar{min-height:42px;background:#f4f6f9;border-bottom-color:#ccd4df}.form-command-bar button{height:27px;border-color:#aeb8c6;background:linear-gradient(#fff,#edf1f6);box-shadow:inset 0 1px 0 #fff;color:#1f2937}.form-command-bar button:hover{background:#fff}.form-fields{grid-template-columns:repeat(12,minmax(0,1fr));align-content:start;gap:8px 12px;padding:14px 16px 22px;background:#fbfbfc}.form-field{grid-column:1/-1;grid-template-columns:150px minmax(0,1fr);min-height:32px;padding:4px 6px;border:1px solid transparent}.form-field:hover{border-color:#b9c1cd;background:#fff}.form-field[data-html5-form-width="half"]{grid-column:span 6}.form-field[data-html5-form-width="third"]{grid-column:span 4}.form-field>label{font-size:12px;font-weight:800;color:#4b5563;align-self:center;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.form-input-control,.form-text-control{width:100%;border:1px solid #aeb8c6;background:#fff;padding:0 7px;color:#17202a;box-shadow:inset 0 1px 2px rgba(15,23,42,.06)}.form-input-control{height:28px}.form-text-control{min-height:64px;resize:none}.form-check-control{display:flex;align-items:center;gap:8px;color:#374151}.form-check-control input{width:16px;height:16px}.form-table-control{border:1px solid #aeb8c6;background:#fff;min-height:108px;overflow:hidden}.form-table-control div{display:grid;grid-template-columns:2fr 1fr 1fr}.form-table-control span{min-height:27px;border-right:1px solid #d7dde6;border-bottom:1px solid #d7dde6;padding:5px 7px;color:#374151;font-size:12px}.form-table-control div:first-child span{background:#eef2f7;font-weight:900}.form-group-control{min-height:44px;border:1px dashed #aeb8c6;background:#f6f8fb;padding:10px;color:#687385;font-weight:800}.form-editor-row{display:grid;gap:7px;padding:10px 12px;border-bottom:1px solid var(--line)}label.form-editor-row{grid-template-columns:88px minmax(0,1fr);align-items:center;text-transform:none}.form-editor-row small{color:var(--muted);font-size:11px}.form-editor-row[data-html5-form-element-editor]{grid-template-columns:1fr 118px 1fr 104px}.form-add-row{display:grid;grid-template-columns:minmax(0,1fr) 118px;gap:8px;padding:12px;border-bottom:1px solid var(--line);background:#fbfcfe}.button.primary{background:var(--brand);border-color:var(--brand);color:#fff;margin:12px}.form-designer-foot small{color:var(--muted);font-weight:800}.form-window[data-html5-form-layout="compact"] .form-fields{gap:4px 8px}.form-window[data-html5-form-layout="compact"] .form-field{min-height:28px}.form-window[data-html5-form-layout="columns"] .form-field{grid-column:span 6}.form-window[data-html5-form-layout="columns"] .form-field[data-html5-form-control="table"],.form-window[data-html5-form-layout="columns"] .form-field[data-html5-form-control="group"]{grid-column:1/-1}
|
.form-designer-body{min-height:0;display:grid;grid-template-columns:minmax(0,1fr) 340px;overflow:hidden}.form-property-panel{min-height:0;overflow:auto;border-left:1px solid var(--line);background:#fff}.property-row{display:grid;grid-template-columns:1fr 132px;gap:8px;padding:12px;border-bottom:1px solid var(--line)}.property-row label,.form-editor-row label{display:grid;gap:4px;font-size:11px;font-weight:900;color:var(--muted);text-transform:uppercase}.property-row input,.property-row select,.form-editor-row input,.form-editor-row select,.form-add-row input,.form-add-row select{height:30px;min-width:0;border:1px solid var(--line);background:#fff;padding:0 8px;color:var(--text);font:13px/1.2 system-ui,-apple-system,Segoe UI,sans-serif;text-transform:none}.form-window{border-color:#b9c1cd;background:#fdfdfd;box-shadow:0 12px 28px rgba(33,43,54,.12)}.form-window-title{justify-content:space-between;height:34px;background:linear-gradient(#f8f9fb,#e9edf3);border-bottom-color:#cbd3df;color:#1f2937}.form-window-title small{font-size:11px;color:#687385;font-weight:800}.form-command-bar{min-height:42px;background:#f4f6f9;border-bottom-color:#ccd4df}.form-command-bar button{height:27px;border-color:#aeb8c6;background:linear-gradient(#fff,#edf1f6);box-shadow:inset 0 1px 0 #fff;color:#1f2937}.form-command-bar button:hover{background:#fff}.form-fields{grid-template-columns:repeat(12,minmax(0,1fr));align-content:start;gap:8px 12px;padding:14px 16px 22px;background:#fbfbfc}.form-field{grid-column:1/-1;grid-template-columns:150px minmax(0,1fr);min-height:32px;padding:4px 6px;border:1px solid transparent}.form-field:hover{border-color:#b9c1cd;background:#fff}.form-field[data-html5-form-width="half"]{grid-column:span 6}.form-field[data-html5-form-width="third"]{grid-column:span 4}.form-field>label{font-size:12px;font-weight:800;color:#4b5563;align-self:center;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.form-input-control,.form-text-control{width:100%;border:1px solid #aeb8c6;background:#fff;padding:0 7px;color:#17202a;box-shadow:inset 0 1px 2px rgba(15,23,42,.06)}.form-input-control{height:28px}.form-text-control{min-height:64px;resize:none}.form-check-control{display:flex;align-items:center;gap:8px;color:#374151}.form-check-control input{width:16px;height:16px}.form-table-control{border:1px solid #aeb8c6;background:#fff;min-height:108px;overflow:hidden}.form-table-control div{display:grid;grid-template-columns:2fr 1fr 1fr}.form-table-control span{min-height:27px;border-right:1px solid #d7dde6;border-bottom:1px solid #d7dde6;padding:5px 7px;color:#374151;font-size:12px}.form-table-control div:first-child span{background:#eef2f7;font-weight:900}.form-group-control{min-height:44px;border:1px dashed #aeb8c6;background:#f6f8fb;padding:10px;color:#687385;font-weight:800}.form-editor-row{display:grid;gap:7px;padding:10px 12px;border-bottom:1px solid var(--line)}label.form-editor-row{grid-template-columns:88px minmax(0,1fr);align-items:center;text-transform:none}.form-editor-row small{color:var(--muted);font-size:11px}.form-editor-row[data-html5-form-element-editor]{grid-template-columns:1fr 118px 1fr 104px}.form-add-row{display:grid;grid-template-columns:minmax(0,1fr) 118px;gap:8px;padding:12px;border-bottom:1px solid var(--line);background:#fbfcfe}.button.primary{background:var(--brand);border-color:var(--brand);color:#fff;margin:12px}.form-designer-foot small{color:var(--muted);font-weight:800}.form-window[data-html5-form-layout="compact"] .form-fields{gap:4px 8px}.form-window[data-html5-form-layout="compact"] .form-field{min-height:28px}.form-window[data-html5-form-layout="columns"] .form-field{grid-column:span 6}.form-window[data-html5-form-layout="columns"] .form-field[data-html5-form-control="table"],.form-window[data-html5-form-layout="columns"] .form-field[data-html5-form-control="group"]{grid-column:1/-1}
|
||||||
.form-empty-structure{grid-column:1/-1;border:1px dashed #aeb8c6;background:#fff;padding:18px;color:#687385}.form-empty-structure strong,.form-empty-structure span{display:block}.form-empty-structure strong{color:#1f2937}
|
.form-empty-structure{grid-column:1/-1;border:1px dashed #aeb8c6;background:#fff;padding:18px;color:#687385}.form-empty-structure strong,.form-empty-structure span{display:block}.form-empty-structure strong{color:#1f2937}
|
||||||
|
.access-workspace{background:#f4f7fb}.access-layout{display:grid;grid-template-columns:300px minmax(0,1fr)340px;height:calc(100vh - 88px)}.access-main{border-top:0;border-bottom:0;overflow:auto}.access-nav,.access-side{overflow:auto}.tree-item[data-html5-access-profile-selected="true"]{background:#f8fbff;border-left:3px solid var(--brand);padding-left:9px}.access-empty{margin:16px;border:1px dashed #aeb8c6;background:#fff;padding:18px;color:#687385}.access-empty strong,.access-empty span{display:block}.access-empty strong{color:#1f2937}.access-profile,.access-plan,.access-result{border-bottom:1px solid var(--line);background:#fff}.access-summary{display:flex;gap:8px;flex-wrap:wrap;padding:10px 14px;border-bottom:1px solid var(--line);background:#f8fbff;color:#687385;font-size:12px;font-weight:800}.access-role-grid,.access-operations,.access-list{display:grid}.access-role-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.access-card{display:grid;gap:3px;min-width:0;padding:10px 12px;border-bottom:1px solid var(--line)}.access-role-grid .access-card:nth-child(odd){border-right:1px solid var(--line)}.access-card strong,.access-card small{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.access-card small{color:var(--muted)}.access-plan-head{display:flex;gap:10px;align-items:center;justify-content:space-between;padding:10px 12px;border-bottom:1px solid var(--line);background:#fbfcfe}.access-plan-head form{margin:0}.access-actions{padding:12px}.access-warnings{margin:0;padding:0;list-style:none}.access-warnings li{padding:8px 12px;border-bottom:1px solid var(--line);color:var(--warn);font-weight:800}.access-json{margin:0;max-height:220px;overflow:auto;padding:12px;background:#fbfaf7;border-top:1px solid var(--line);font:12px/1.5 ui-monospace,SFMono-Regular,Consolas,monospace;white-space:pre-wrap}.access-main .primary{background:var(--brand);border-color:var(--brand);color:#fff}
|
||||||
@media(max-width:980px){.layout{grid-template-columns:1fr;height:auto}.tree,.inspector{max-height:320px}.editor{min-height:520px}.hero{display:block}.shell{padding:16px}}
|
@media(max-width:980px){.layout{grid-template-columns:1fr;height:auto}.tree,.inspector{max-height:320px}.editor{min-height:520px}.hero{display:block}.shell{padding:16px}}
|
||||||
|
@media(max-width:980px){.access-layout{grid-template-columns:1fr;height:auto}.access-nav,.access-side{max-height:360px}.access-role-grid{grid-template-columns:1fr}.access-role-grid .access-card:nth-child(odd){border-right:0}}
|
||||||
@media(max-width:980px){.form-designer-body{grid-template-columns:1fr}.form-property-panel{border-left:0;border-top:1px solid var(--line)}.form-field[data-html5-form-width="half"],.form-field[data-html5-form-width="third"]{grid-column:1/-1}.form-editor-row[data-html5-form-element-editor]{grid-template-columns:1fr}.property-row{grid-template-columns:1fr}}
|
@media(max-width:980px){.form-designer-body{grid-template-columns:1fr}.form-property-panel{border-left:0;border-top:1px solid var(--line)}.form-field[data-html5-form-width="half"],.form-field[data-html5-form-width="third"]{grid-column:1/-1}.form-editor-row[data-html5-form-element-editor]{grid-template-columns:1fr}.property-row{grid-template-columns:1fr}}
|
||||||
@media(max-width:980px){.setup-layout{grid-template-columns:1fr}.setup-metrics{grid-template-columns:1fr 1fr}.settings-form{grid-template-columns:1fr}}
|
@media(max-width:980px){.setup-layout{grid-template-columns:1fr}.setup-metrics{grid-template-columns:1fr 1fr}.settings-form{grid-template-columns:1fr}}
|
||||||
|
|||||||
@@ -1610,6 +1610,26 @@ def test_import_supports_structure_only_indexing(monkeypatch, tmp_path: Path):
|
|||||||
assert captured_extension_call["request"].dry_run is True
|
assert captured_extension_call["request"].dry_run is True
|
||||||
assert captured_extension_call["request"].payload["profile"]["roles"] == ["Роль.Менеджер"]
|
assert captured_extension_call["request"].payload["profile"]["roles"] == ["Роль.Менеджер"]
|
||||||
|
|
||||||
|
access_page = client.get(f"/html5/projects/{project_id}/access", params={"profile": "НовыйПрофильHTTP"})
|
||||||
|
assert_html5_response_contract(
|
||||||
|
access_page,
|
||||||
|
'data-html5-page="access"',
|
||||||
|
"НовыйПрофильHTTP",
|
||||||
|
"План публикации",
|
||||||
|
"Dry-run в 1С",
|
||||||
|
full_page=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
access_plan = client.get(
|
||||||
|
f"/html5/projects/{project_id}/access/profiles/{quote('НовыйПрофильHTTP')}/plan"
|
||||||
|
)
|
||||||
|
assert_html5_response_contract(access_plan, "CREATE_ACCESS_PROFILE", "ADD_ROLE_TO_PROFILE")
|
||||||
|
|
||||||
|
access_dry_run = client.post(
|
||||||
|
f"/html5/projects/{project_id}/access/profiles/{quote('НовыйПрофильHTTP')}/publish-dry-run"
|
||||||
|
)
|
||||||
|
assert_html5_response_contract(access_dry_run, "Ответ расширения", "READY", "access.profile.apply")
|
||||||
|
|
||||||
tree = client.get(f"/projects/{project_id}/metadata/tree")
|
tree = client.get(f"/projects/{project_id}/metadata/tree")
|
||||||
assert tree.status_code == 200
|
assert tree.status_code == 200
|
||||||
root = tree.json()["root"]
|
root = tree.json()["root"]
|
||||||
|
|||||||
Reference in New Issue
Block a user