Compare commits
6
Commits
925dc33696
...
v0.1.21
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54178ee56c | ||
|
|
10e7597612 | ||
|
|
142ccbc587 | ||
|
|
f75ad48d78 | ||
|
|
5a9e8f79f9 | ||
|
|
fbea74a74b |
@@ -48,6 +48,12 @@ module and server restrictions, lower-scope visibility, activation, save, and
|
||||
irreversible deletion. This ensures F1 explains secret custody and the effect on
|
||||
dependent connections from system, tenant, group, user, and personal surfaces.
|
||||
|
||||
The source inventory treats literal `helpContextId` and
|
||||
`data-help-context-id` declarations as authored help associations, including a
|
||||
native control nested in `FormField`. Dynamic context expressions remain
|
||||
separate evidence and generic derived fallbacks remain in the richer-help
|
||||
candidate queue.
|
||||
|
||||
The generated `help_review_candidates` list is therefore a content-depth queue,
|
||||
not a list of controls on which F1 cannot work. It should prioritize:
|
||||
|
||||
@@ -57,6 +63,14 @@ not a list of controls on which F1 cannot work. It should prioritize:
|
||||
4. provider authority, synchronization, conflict, and outcome unknown;
|
||||
5. fields whose consequences are not evident from their label.
|
||||
|
||||
The shared browser conformance journey mounts the production Help menu and
|
||||
resolver. It proves that F1 uses the focused control rather than only the page,
|
||||
maps an exact retention action to Policy-owned administrator documentation,
|
||||
retains the page context as fallback for derived actions, exposes an accessible
|
||||
modal at narrow widths, closes with Escape, and restores focus to the triggering
|
||||
control. Module journeys should add their own exact high-risk mappings; they do
|
||||
not need to reimplement the keyboard or dialog mechanics.
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
@@ -74,3 +88,10 @@ The check must report:
|
||||
- no duplicate stable IDs;
|
||||
- no undeclared public WebUI surface;
|
||||
- no stale runtime route or endpoint declaration.
|
||||
|
||||
Browser acceptance is part of the focused workspace gate and can be run alone:
|
||||
|
||||
```bash
|
||||
cd /mnt/DATA/git/govoplan-core/webui
|
||||
npm run test:conformance
|
||||
```
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "govoplan-core"
|
||||
version = "0.1.18"
|
||||
version = "0.1.21"
|
||||
description = "Reusable GovOPlaN platform core, access, tenancy, and RBAC components."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
|
||||
@@ -5,6 +5,7 @@ from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from typing import Literal, Protocol, runtime_checkable
|
||||
|
||||
from govoplan_core.core.access import PrincipalRef
|
||||
from govoplan_core.core.external_references import (
|
||||
SOURCE_AUTHORITY_MODES,
|
||||
SourceAuthorityMode,
|
||||
@@ -23,6 +24,8 @@ CAPABILITY_DATASOURCE_CATALOGUE = "datasources.catalogue"
|
||||
CAPABILITY_DATASOURCE_LIFECYCLE = "datasources.lifecycle"
|
||||
CAPABILITY_DATASOURCE_PUBLICATION = "datasources.publication"
|
||||
CAPABILITY_DATASOURCE_ORIGINS = "connectors.datasourceOrigins"
|
||||
CAPABILITY_DATASOURCE_ARTIFACT_BACKENDS = "datasources.artifactBackends"
|
||||
CAPABILITY_POLICY_DATASOURCE_VISIBILITY = "policy.datasourceVisibility"
|
||||
|
||||
DatasourceMode = Literal["live", "cached", "static"]
|
||||
DatasourceKind = Literal[
|
||||
@@ -37,6 +40,12 @@ DatasourceKind = Literal[
|
||||
]
|
||||
DatasourceShape = Literal["tabular", "document", "binary", "directory", "stream"]
|
||||
DatasourceConsistency = Literal["current", "live", "frozen"]
|
||||
DatasourceVisibilityAction = Literal["discover", "read"]
|
||||
DatasourcePublicationStatus = Literal[
|
||||
"published",
|
||||
"published_with_warnings",
|
||||
"review_required",
|
||||
]
|
||||
|
||||
|
||||
class DatasourceError(ValueError):
|
||||
@@ -64,6 +73,7 @@ class DatasourceField:
|
||||
name: str
|
||||
data_type: str
|
||||
nullable: bool = True
|
||||
classification: str = "internal"
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
@@ -84,6 +94,8 @@ class DatasourceGovernance:
|
||||
classification: str = "internal"
|
||||
privacy_profile_ref: str | None = None
|
||||
retention_policy_ref: str | None = None
|
||||
access_policy_ref: str | None = None
|
||||
visibility_policy: Mapping[str, object] = field(default_factory=dict)
|
||||
hold_refs: tuple[str, ...] = ()
|
||||
publication_state: str = "draft"
|
||||
transfer_agreement_ref: str | None = None
|
||||
@@ -154,6 +166,12 @@ class DatasourceGovernance:
|
||||
retention_policy_ref=_optional_governance_text(
|
||||
source.get("retention_policy_ref")
|
||||
),
|
||||
access_policy_ref=_optional_governance_text(
|
||||
source.get("access_policy_ref")
|
||||
),
|
||||
visibility_policy=_governance_mapping(
|
||||
source.get("visibility_policy")
|
||||
),
|
||||
hold_refs=_governance_texts(source.get("hold_refs")),
|
||||
publication_state=str(source.get("publication_state") or "draft"),
|
||||
transfer_agreement_ref=_optional_governance_text(
|
||||
@@ -185,6 +203,8 @@ class DatasourceGovernance:
|
||||
"classification": self.classification,
|
||||
"privacy_profile_ref": self.privacy_profile_ref,
|
||||
"retention_policy_ref": self.retention_policy_ref,
|
||||
"access_policy_ref": self.access_policy_ref,
|
||||
"visibility_policy": dict(self.visibility_policy),
|
||||
"hold_refs": list(self.hold_refs),
|
||||
"publication_state": self.publication_state,
|
||||
"transfer_agreement_ref": self.transfer_agreement_ref,
|
||||
@@ -197,6 +217,39 @@ class DatasourceGovernance:
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class DatasourceVisibilityPolicyRequest:
|
||||
tenant_id: str
|
||||
datasource_ref: str
|
||||
principal: PrincipalRef
|
||||
action: DatasourceVisibilityAction
|
||||
classification: str = "internal"
|
||||
policy_ref: str | None = None
|
||||
consistency: DatasourceConsistency = "current"
|
||||
materialization_ref: str | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class DatasourceVisibilityPolicyDecision:
|
||||
allowed: bool
|
||||
reason: str | None = None
|
||||
policies: tuple[Mapping[str, object], ...] = ()
|
||||
decision_ref: str | None = None
|
||||
provenance: Mapping[str, object] = field(default_factory=dict)
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class DatasourceVisibilityPolicyProvider(Protocol):
|
||||
"""Optionally tighten Datasources-owned local visibility policy."""
|
||||
|
||||
def decide_datasource_visibility(
|
||||
self,
|
||||
session: object,
|
||||
*,
|
||||
request: DatasourceVisibilityPolicyRequest,
|
||||
) -> DatasourceVisibilityPolicyDecision: ...
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class DatasourceDescriptor:
|
||||
ref: str
|
||||
@@ -309,12 +362,73 @@ class DatasourceStageInput:
|
||||
governance: DatasourceGovernance | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class DatasourceArtifactReference:
|
||||
"""Immutable provider-neutral reference to a durable tabular payload.
|
||||
|
||||
The producer owns creation of the payload. Datasources pins its locator,
|
||||
checksum and declared shape without importing the artifact-owning module;
|
||||
a configured payload backend verifies integrity and provides bounded reads.
|
||||
"""
|
||||
|
||||
backend: str
|
||||
locator: str
|
||||
checksum: str
|
||||
row_count: int
|
||||
byte_count: int
|
||||
schema: tuple[DatasourceField, ...]
|
||||
fingerprint: str
|
||||
media_type: str = "application/x-ndjson"
|
||||
checkpoint: Mapping[str, object] = field(default_factory=dict)
|
||||
metadata: Mapping[str, object] = field(default_factory=dict)
|
||||
validation: Mapping[str, object] = field(default_factory=dict)
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class DatasourceArtifactBackend(Protocol):
|
||||
"""Storage-module boundary for immutable artifact-backed tabular data."""
|
||||
|
||||
backend: str
|
||||
|
||||
def verify(
|
||||
self,
|
||||
session: object,
|
||||
*,
|
||||
tenant_id: str,
|
||||
artifact: DatasourceArtifactReference,
|
||||
) -> None: ...
|
||||
|
||||
def read_rows(
|
||||
self,
|
||||
session: object,
|
||||
*,
|
||||
tenant_id: str,
|
||||
artifact: DatasourceArtifactReference,
|
||||
offset: int,
|
||||
limit: int,
|
||||
) -> Sequence[Mapping[str, object]]: ...
|
||||
|
||||
def delete(
|
||||
self,
|
||||
session: object,
|
||||
*,
|
||||
tenant_id: str,
|
||||
artifact: DatasourceArtifactReference,
|
||||
) -> None: ...
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class DatasourceArtifactBackendProvider(Protocol):
|
||||
def artifact_backends(self) -> Sequence[DatasourceArtifactBackend]: ...
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class DatasourcePublicationRequest:
|
||||
producer_module: str
|
||||
producer_run_ref: str
|
||||
idempotency_key: str
|
||||
rows: tuple[Mapping[str, object], ...]
|
||||
rows: tuple[Mapping[str, object], ...] | None = None
|
||||
artifact: DatasourceArtifactReference | None = None
|
||||
target_datasource_ref: str | None = None
|
||||
name: str | None = None
|
||||
source_name: str | None = None
|
||||
@@ -331,7 +445,7 @@ class DatasourcePublicationRequest:
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class DatasourcePublicationResult:
|
||||
ref: str
|
||||
status: str
|
||||
status: DatasourcePublicationStatus
|
||||
datasource: DatasourceDescriptor
|
||||
materialization: DatasourceMaterialization
|
||||
replayed: bool = False
|
||||
@@ -564,6 +678,17 @@ def datasource_catalogue(registry: object | None) -> DatasourceCatalogueProvider
|
||||
return capability if isinstance(capability, DatasourceCatalogueProvider) else None
|
||||
|
||||
|
||||
def datasource_artifact_backend_provider(
|
||||
registry: object | None,
|
||||
) -> DatasourceArtifactBackendProvider | None:
|
||||
capability = _capability(registry, CAPABILITY_DATASOURCE_ARTIFACT_BACKENDS)
|
||||
return (
|
||||
capability
|
||||
if isinstance(capability, DatasourceArtifactBackendProvider)
|
||||
else None
|
||||
)
|
||||
|
||||
|
||||
def datasource_lifecycle(registry: object | None) -> DatasourceLifecycleProvider | None:
|
||||
capability = _capability(registry, CAPABILITY_DATASOURCE_LIFECYCLE)
|
||||
return capability if isinstance(capability, DatasourceLifecycleProvider) else None
|
||||
@@ -581,6 +706,13 @@ def datasource_origins(registry: object | None) -> DatasourceOriginProvider | No
|
||||
return capability if isinstance(capability, DatasourceOriginProvider) else None
|
||||
|
||||
|
||||
def datasource_visibility_policy_provider(
|
||||
registry: object | None,
|
||||
) -> DatasourceVisibilityPolicyProvider | None:
|
||||
capability = _capability(registry, CAPABILITY_POLICY_DATASOURCE_VISIBILITY)
|
||||
return capability if isinstance(capability, DatasourceVisibilityPolicyProvider) else None
|
||||
|
||||
|
||||
def _capability(registry: object | None, name: str) -> object | None:
|
||||
if (
|
||||
registry is None
|
||||
@@ -613,9 +745,15 @@ def _governance_mapping(value: object) -> Mapping[str, object]:
|
||||
|
||||
__all__ = [
|
||||
"CAPABILITY_DATASOURCE_CATALOGUE",
|
||||
"CAPABILITY_DATASOURCE_ARTIFACT_BACKENDS",
|
||||
"CAPABILITY_DATASOURCE_LIFECYCLE",
|
||||
"CAPABILITY_DATASOURCE_ORIGINS",
|
||||
"CAPABILITY_DATASOURCE_PUBLICATION",
|
||||
"CAPABILITY_POLICY_DATASOURCE_VISIBILITY",
|
||||
"DatasourceAccessError",
|
||||
"DatasourceArtifactReference",
|
||||
"DatasourceArtifactBackend",
|
||||
"DatasourceArtifactBackendProvider",
|
||||
"DatasourceCatalogueProvider",
|
||||
"DatasourceConsistency",
|
||||
"DatasourceDescriptor",
|
||||
@@ -631,6 +769,10 @@ __all__ = [
|
||||
"DatasourceOriginProvider",
|
||||
"DatasourceOriginReadRequest",
|
||||
"DatasourceOriginReadResult",
|
||||
"DatasourcePublicationProvider",
|
||||
"DatasourcePublicationRequest",
|
||||
"DatasourcePublicationResult",
|
||||
"DatasourcePublicationStatus",
|
||||
"DatasourceReadRequest",
|
||||
"DatasourceReadResult",
|
||||
"DatasourceShape",
|
||||
@@ -638,7 +780,14 @@ __all__ = [
|
||||
"DatasourceStageInput",
|
||||
"DatasourceUnavailableError",
|
||||
"DatasourceValidationError",
|
||||
"DatasourceVisibilityAction",
|
||||
"DatasourceVisibilityPolicyDecision",
|
||||
"DatasourceVisibilityPolicyProvider",
|
||||
"DatasourceVisibilityPolicyRequest",
|
||||
"datasource_catalogue",
|
||||
"datasource_artifact_backend_provider",
|
||||
"datasource_lifecycle",
|
||||
"datasource_origins",
|
||||
"datasource_publication",
|
||||
"datasource_visibility_policy_provider",
|
||||
]
|
||||
|
||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from typing import Protocol, runtime_checkable
|
||||
|
||||
from govoplan_core.core.access import ResourceAccessExplanationProvider
|
||||
@@ -10,6 +11,48 @@ from govoplan_core.core.access import ResourceAccessExplanationProvider
|
||||
CAPABILITY_FILES_ACCESS = "files.access"
|
||||
CAPABILITY_FILES_ARTIFACT_STORE = "files.artifact_store"
|
||||
CAPABILITY_FILES_POSTBOX_REFERENCES = "files.postbox_references"
|
||||
CAPABILITY_FILES_TABULAR_CONTENT = "files.tabular_content"
|
||||
|
||||
|
||||
class ManagedTabularFileError(ValueError):
|
||||
"""Stable base error for exact-version managed tabular file access."""
|
||||
|
||||
|
||||
class ManagedTabularFileNotFoundError(ManagedTabularFileError):
|
||||
pass
|
||||
|
||||
|
||||
class ManagedTabularFileAccessError(ManagedTabularFileError):
|
||||
pass
|
||||
|
||||
|
||||
class ManagedTabularFileUnavailableError(ManagedTabularFileError):
|
||||
pass
|
||||
|
||||
|
||||
class ManagedTabularFileValidationError(ManagedTabularFileError):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ManagedTabularFile:
|
||||
"""Authorized metadata for one immutable managed file version."""
|
||||
|
||||
file_asset_id: str
|
||||
file_version_id: str
|
||||
filename: str
|
||||
display_path: str
|
||||
content_type: str | None
|
||||
size_bytes: int
|
||||
sha256: str
|
||||
updated_at: datetime | None = None
|
||||
current_version: bool = True
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ManagedTabularFileContent:
|
||||
file: ManagedTabularFile
|
||||
payload: bytes
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
@@ -91,6 +134,39 @@ class PostboxFileReferenceProvider(Protocol):
|
||||
) -> tuple[PostboxFileReferenceRef, ...]: ...
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class ManagedTabularFileProvider(Protocol):
|
||||
"""List and open authorized CSV/XLSX content without exposing Files internals."""
|
||||
|
||||
def list_tabular_files(
|
||||
self,
|
||||
session: object,
|
||||
principal: object,
|
||||
*,
|
||||
query: str = "",
|
||||
limit: int = 100,
|
||||
) -> tuple[ManagedTabularFile, ...]: ...
|
||||
|
||||
def get_tabular_file(
|
||||
self,
|
||||
session: object,
|
||||
principal: object,
|
||||
*,
|
||||
file_asset_id: str,
|
||||
file_version_id: str | None = None,
|
||||
) -> ManagedTabularFile | None: ...
|
||||
|
||||
def read_tabular_file(
|
||||
self,
|
||||
session: object,
|
||||
principal: object,
|
||||
*,
|
||||
file_asset_id: str,
|
||||
file_version_id: str,
|
||||
max_bytes: int,
|
||||
) -> ManagedTabularFileContent: ...
|
||||
|
||||
|
||||
def postbox_file_reference_provider(
|
||||
registry: object | None,
|
||||
) -> PostboxFileReferenceProvider | None:
|
||||
@@ -107,3 +183,46 @@ def postbox_file_reference_provider(
|
||||
"PostboxFileReferenceProvider"
|
||||
)
|
||||
return provider
|
||||
|
||||
|
||||
def managed_tabular_file_provider(
|
||||
registry: object | None,
|
||||
) -> ManagedTabularFileProvider | None:
|
||||
if (
|
||||
registry is None
|
||||
or not hasattr(registry, "has_capability")
|
||||
or not registry.has_capability(CAPABILITY_FILES_TABULAR_CONTENT)
|
||||
):
|
||||
return None
|
||||
provider = registry.require_capability(CAPABILITY_FILES_TABULAR_CONTENT)
|
||||
if not isinstance(provider, ManagedTabularFileProvider):
|
||||
raise TypeError(
|
||||
"files.tabular_content provider does not implement "
|
||||
"ManagedTabularFileProvider"
|
||||
)
|
||||
return provider
|
||||
|
||||
|
||||
__all__ = [
|
||||
"CAPABILITY_FILES_ACCESS",
|
||||
"CAPABILITY_FILES_ARTIFACT_STORE",
|
||||
"CAPABILITY_FILES_POSTBOX_REFERENCES",
|
||||
"CAPABILITY_FILES_TABULAR_CONTENT",
|
||||
"FileAccessProvider",
|
||||
"ManagedArtifactRef",
|
||||
"ManagedArtifactStore",
|
||||
"ManagedArtifactWriteRequest",
|
||||
"ManagedTabularFile",
|
||||
"ManagedTabularFileAccessError",
|
||||
"ManagedTabularFileContent",
|
||||
"ManagedTabularFileError",
|
||||
"ManagedTabularFileNotFoundError",
|
||||
"ManagedTabularFileProvider",
|
||||
"ManagedTabularFileUnavailableError",
|
||||
"ManagedTabularFileValidationError",
|
||||
"PostboxFileReferenceProvider",
|
||||
"PostboxFileReferenceRef",
|
||||
"PostboxFileReferenceRequest",
|
||||
"managed_tabular_file_provider",
|
||||
"postbox_file_reference_provider",
|
||||
]
|
||||
|
||||
@@ -3,11 +3,14 @@ from __future__ import annotations
|
||||
import unittest
|
||||
|
||||
from govoplan_core.core.datasources import (
|
||||
CAPABILITY_DATASOURCE_ARTIFACT_BACKENDS,
|
||||
CAPABILITY_DATASOURCE_CATALOGUE,
|
||||
CAPABILITY_DATASOURCE_LIFECYCLE,
|
||||
CAPABILITY_DATASOURCE_ORIGINS,
|
||||
CAPABILITY_DATASOURCE_PUBLICATION,
|
||||
CAPABILITY_POLICY_DATASOURCE_VISIBILITY,
|
||||
DatasourceCatalogueProvider,
|
||||
DatasourceArtifactBackendProvider,
|
||||
DatasourceDescriptor,
|
||||
DatasourceField,
|
||||
DatasourceLifecycleProvider,
|
||||
@@ -19,10 +22,14 @@ from govoplan_core.core.datasources import (
|
||||
DatasourcePublicationResult,
|
||||
DatasourceReadResult,
|
||||
DatasourceStage,
|
||||
DatasourceVisibilityPolicyDecision,
|
||||
DatasourceVisibilityPolicyProvider,
|
||||
datasource_catalogue,
|
||||
datasource_artifact_backend_provider,
|
||||
datasource_lifecycle,
|
||||
datasource_origins,
|
||||
datasource_publication,
|
||||
datasource_visibility_policy_provider,
|
||||
)
|
||||
from govoplan_core.core.modules import ModuleContext, ModuleManifest
|
||||
from govoplan_core.core.registry import PlatformRegistry
|
||||
@@ -155,6 +162,13 @@ class _Provider:
|
||||
truncated=False,
|
||||
)
|
||||
|
||||
def artifact_backends(self):
|
||||
return ()
|
||||
|
||||
def decide_datasource_visibility(self, session, *, request):
|
||||
del session, request
|
||||
return DatasourceVisibilityPolicyDecision(allowed=True)
|
||||
|
||||
|
||||
class DatasourceContractTests(unittest.TestCase):
|
||||
def test_capabilities_are_runtime_checkable_and_resolved_without_modules(self) -> None:
|
||||
@@ -163,6 +177,8 @@ class DatasourceContractTests(unittest.TestCase):
|
||||
self.assertIsInstance(provider, DatasourceLifecycleProvider)
|
||||
self.assertIsInstance(provider, DatasourcePublicationProvider)
|
||||
self.assertIsInstance(provider, DatasourceOriginProvider)
|
||||
self.assertIsInstance(provider, DatasourceArtifactBackendProvider)
|
||||
self.assertIsInstance(provider, DatasourceVisibilityPolicyProvider)
|
||||
registry = PlatformRegistry()
|
||||
registry.register(
|
||||
ModuleManifest(
|
||||
@@ -174,6 +190,8 @@ class DatasourceContractTests(unittest.TestCase):
|
||||
CAPABILITY_DATASOURCE_LIFECYCLE: lambda context: provider,
|
||||
CAPABILITY_DATASOURCE_PUBLICATION: lambda context: provider,
|
||||
CAPABILITY_DATASOURCE_ORIGINS: lambda context: provider,
|
||||
CAPABILITY_DATASOURCE_ARTIFACT_BACKENDS: lambda context: provider,
|
||||
CAPABILITY_POLICY_DATASOURCE_VISIBILITY: lambda context: provider,
|
||||
},
|
||||
)
|
||||
)
|
||||
@@ -183,6 +201,8 @@ class DatasourceContractTests(unittest.TestCase):
|
||||
self.assertIs(provider, datasource_lifecycle(registry))
|
||||
self.assertIs(provider, datasource_publication(registry))
|
||||
self.assertIs(provider, datasource_origins(registry))
|
||||
self.assertIs(provider, datasource_artifact_backend_provider(registry))
|
||||
self.assertIs(provider, datasource_visibility_policy_provider(registry))
|
||||
self.assertIsNone(datasource_catalogue(PlatformRegistry()))
|
||||
|
||||
def test_descriptor_distinguishes_mode_kind_shape_and_materialization(self) -> None:
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from govoplan_core.core.files import (
|
||||
CAPABILITY_FILES_TABULAR_CONTENT,
|
||||
ManagedTabularFile,
|
||||
ManagedTabularFileContent,
|
||||
ManagedTabularFileProvider,
|
||||
managed_tabular_file_provider,
|
||||
)
|
||||
|
||||
|
||||
class _Provider:
|
||||
def list_tabular_files(self, session, principal, *, query="", limit=100):
|
||||
del session, principal, query, limit
|
||||
return ()
|
||||
|
||||
def get_tabular_file(
|
||||
self,
|
||||
session,
|
||||
principal,
|
||||
*,
|
||||
file_asset_id,
|
||||
file_version_id=None,
|
||||
):
|
||||
del session, principal, file_asset_id, file_version_id
|
||||
return None
|
||||
|
||||
def read_tabular_file(
|
||||
self,
|
||||
session,
|
||||
principal,
|
||||
*,
|
||||
file_asset_id,
|
||||
file_version_id,
|
||||
max_bytes,
|
||||
):
|
||||
del session, principal, file_asset_id, file_version_id, max_bytes
|
||||
file = ManagedTabularFile(
|
||||
file_asset_id="asset-1",
|
||||
file_version_id="version-1",
|
||||
filename="source.csv",
|
||||
display_path="Imports/source.csv",
|
||||
content_type="text/csv",
|
||||
size_bytes=8,
|
||||
sha256="a" * 64,
|
||||
updated_at=datetime(2026, 8, 21, tzinfo=UTC),
|
||||
)
|
||||
return ManagedTabularFileContent(file=file, payload=b"id\n1\n")
|
||||
|
||||
|
||||
class _Registry:
|
||||
def __init__(self, provider):
|
||||
self.provider = provider
|
||||
|
||||
def has_capability(self, name):
|
||||
return name == CAPABILITY_FILES_TABULAR_CONTENT
|
||||
|
||||
def require_capability(self, name):
|
||||
if not self.has_capability(name):
|
||||
raise KeyError(name)
|
||||
return self.provider
|
||||
|
||||
|
||||
class ManagedTabularFileContractTests(unittest.TestCase):
|
||||
def test_runtime_protocol_and_registry_resolution(self) -> None:
|
||||
provider = _Provider()
|
||||
|
||||
self.assertIsInstance(provider, ManagedTabularFileProvider)
|
||||
self.assertIs(provider, managed_tabular_file_provider(_Registry(provider)))
|
||||
self.assertIsNone(managed_tabular_file_provider(None))
|
||||
|
||||
def test_exact_version_content_retains_safe_metadata(self) -> None:
|
||||
result = _Provider().read_tabular_file(
|
||||
object(),
|
||||
object(),
|
||||
file_asset_id="asset-1",
|
||||
file_version_id="version-1",
|
||||
max_bytes=100,
|
||||
)
|
||||
|
||||
self.assertEqual("version-1", result.file.file_version_id)
|
||||
self.assertEqual("a" * 64, result.file.sha256)
|
||||
self.assertEqual(b"id\n1\n", result.payload)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -10,6 +10,7 @@ from fastapi import APIRouter, Depends, FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from govoplan_core.auth import ApiPrincipal, get_api_principal
|
||||
from govoplan_core.admin.models import SystemSettings
|
||||
from govoplan_core.celery_app import _run_tenant_worker_batches
|
||||
from govoplan_core.core.access import PrincipalRef
|
||||
from govoplan_core.core.lifecycle import require_module_active
|
||||
@@ -26,6 +27,7 @@ from govoplan_core.core.module_entitlements import (
|
||||
from govoplan_core.core.modules import ModuleContext, ModuleManifest
|
||||
from govoplan_core.core.registry import PlatformRegistry
|
||||
from govoplan_core.db.session import configure_database, get_database
|
||||
from govoplan_core.db.base import Base
|
||||
from govoplan_core.server.platform import create_platform_router
|
||||
from govoplan_core.tenancy.scope import Tenant, create_scope_tables
|
||||
|
||||
@@ -257,6 +259,10 @@ class TenantModuleEntitlementRouteTests(unittest.TestCase):
|
||||
root = Path(tempfile.mkdtemp(prefix="govoplan-entitlement-test-"))
|
||||
configure_database(f"sqlite:///{root / 'test.db'}")
|
||||
create_scope_tables(get_database().engine)
|
||||
Base.metadata.create_all(
|
||||
bind=get_database().engine,
|
||||
tables=[SystemSettings.__table__],
|
||||
)
|
||||
self.manifests = (
|
||||
ModuleManifest(id="access", name="Access", version="test"),
|
||||
ModuleManifest(
|
||||
|
||||
@@ -26,6 +26,7 @@ import WorkspaceFrame from "../src/components/WorkspaceFrame";
|
||||
import WorkspaceLayout from "../src/components/WorkspaceLayout";
|
||||
import WorkspaceActionBar from "../src/components/WorkspaceActionBar";
|
||||
import BreadcrumbBar from "../src/layout/BreadcrumbBar";
|
||||
import HelpMenu from "../src/layout/HelpMenu";
|
||||
import { useGuardedNavigate } from "../src/components/UnsavedChangesGuard";
|
||||
import {
|
||||
createQuickAccessLaunchContext,
|
||||
@@ -149,6 +150,7 @@ export default function ConformanceApp() {
|
||||
</section>
|
||||
|
||||
<LaunchContextScenario />
|
||||
{new URLSearchParams(location.search).has("help") ? <HelpConformanceScenario /> : null}
|
||||
</PageLayout>
|
||||
|
||||
<Dialog
|
||||
@@ -170,6 +172,32 @@ export default function ConformanceApp() {
|
||||
);
|
||||
}
|
||||
|
||||
function HelpConformanceScenario() {
|
||||
return (
|
||||
<section className="conformance-section" aria-labelledby="help-heading">
|
||||
<h2 id="help-heading">Kontextsensitive Hilfe</h2>
|
||||
<p>F1 löst den Hilfekontext des fokussierten Bedienelements auf und kehrt nach dem Schließen dorthin zurück.</p>
|
||||
<ActionToolbar surface="subtle">
|
||||
<Button
|
||||
variant="danger"
|
||||
data-testid="f1-retention-action"
|
||||
helpContextId="policy.retention.action.apply"
|
||||
helpModuleId="policy"
|
||||
>
|
||||
Aufbewahrungsregeln anwenden
|
||||
</Button>
|
||||
<Button
|
||||
data-testid="f1-fallback-action"
|
||||
interfaceId="core.conformance.action.review"
|
||||
>
|
||||
Allgemeine Aktion prüfen
|
||||
</Button>
|
||||
<HelpMenu auth={null} />
|
||||
</ActionToolbar>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function QuickAccessScenario() {
|
||||
const location = useLocation();
|
||||
const mode = new URLSearchParams(location.search).get("quick-access");
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
const installedModules: Array<never> = [];
|
||||
|
||||
export default installedModules;
|
||||
@@ -3,6 +3,9 @@ import ReactDOM from "react-dom/client";
|
||||
import { BrowserRouter } from "react-router";
|
||||
import ConformanceApp from "./ConformanceApp";
|
||||
import { UnsavedChangesProvider } from "../src/components/UnsavedChangesGuard";
|
||||
import { PlatformLanguageProvider } from "../src/i18n/LanguageContext";
|
||||
import { PlatformModulesProvider } from "../src/platform/ModuleContext";
|
||||
import type { PlatformWebModule } from "../src/types";
|
||||
import "../src/styles/tokens.css";
|
||||
import "../src/styles/layout.css";
|
||||
import "../src/styles/forms.css";
|
||||
@@ -16,12 +19,28 @@ import "./conformance.css";
|
||||
const theme = new URLSearchParams(window.location.search).get("theme");
|
||||
if (theme === "dark" || theme === "light") document.documentElement.dataset.theme = theme;
|
||||
|
||||
const CONFORMANCE_MODULES: PlatformWebModule[] = [{
|
||||
id: "policy",
|
||||
label: "Richtlinien",
|
||||
version: "1",
|
||||
helpContexts: [{
|
||||
id: "policy.retention.action.apply",
|
||||
topic_id: "policy.admin.retention-and-disposition",
|
||||
title: "Aufbewahrungsregeln sicher anwenden",
|
||||
documentation_types: ["admin"]
|
||||
}]
|
||||
}];
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<BrowserRouter>
|
||||
<UnsavedChangesProvider>
|
||||
<ConformanceApp />
|
||||
</UnsavedChangesProvider>
|
||||
<PlatformModulesProvider modules={CONFORMANCE_MODULES}>
|
||||
<PlatformLanguageProvider preferredLanguageCode="de">
|
||||
<UnsavedChangesProvider>
|
||||
<ConformanceApp />
|
||||
</UnsavedChangesProvider>
|
||||
</PlatformLanguageProvider>
|
||||
</PlatformModulesProvider>
|
||||
</BrowserRouter>
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
@@ -110,6 +110,44 @@ test("Quick Access preserves focus and adapts to a narrow viewport", async ({ pa
|
||||
await expect(filesTrigger).toBeFocused();
|
||||
});
|
||||
|
||||
test("F1 resolves focused high-risk help and restores focus accessibly", async ({ page }) => {
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await page.goto("/?theme=light&help=1");
|
||||
|
||||
const retentionAction = page.getByTestId("f1-retention-action");
|
||||
await retentionAction.focus();
|
||||
await page.keyboard.press("F1");
|
||||
|
||||
const dialog = page.getByRole("dialog", { name: "Kontexthilfe" });
|
||||
await expect(dialog).toBeVisible();
|
||||
await expect(dialog.locator("[data-help-context]"))
|
||||
.toHaveAttribute("data-help-context", "policy.retention.action.apply");
|
||||
await expect(dialog.getByRole("heading", { level: 3 }))
|
||||
.toHaveText("Aufbewahrungsregeln anwenden");
|
||||
await expect(dialog).toContainText("topic=policy.admin.retention-and-disposition");
|
||||
await expect(dialog.getByRole("button", { name: "Administrator-Dokumentation öffnen" }))
|
||||
.toBeVisible();
|
||||
await expectNoAccessibilityViolations(page);
|
||||
|
||||
const bounds = await dialog.boundingBox();
|
||||
expect(bounds?.x).toBeGreaterThanOrEqual(0);
|
||||
expect((bounds?.x ?? 0) + (bounds?.width ?? 0)).toBeLessThanOrEqual(390);
|
||||
|
||||
await page.keyboard.press("Escape");
|
||||
await expect(dialog).toBeHidden();
|
||||
await expect(retentionAction).toBeFocused();
|
||||
|
||||
const fallbackAction = page.getByTestId("f1-fallback-action");
|
||||
await fallbackAction.focus();
|
||||
await page.keyboard.press("F1");
|
||||
const fallbackDialog = page.getByRole("dialog", { name: "Kontexthilfe" });
|
||||
await expect(fallbackDialog.locator("[data-help-context]"))
|
||||
.toHaveAttribute("data-help-context", "core.conformance.action.review");
|
||||
await expect(fallbackDialog).toContainText("fallback_context=campaigns.list");
|
||||
await page.keyboard.press("Escape");
|
||||
await expect(fallbackAction).toBeFocused();
|
||||
});
|
||||
|
||||
test("View focus has a deliberate permission-derived all-tools escape", async ({ page }) => {
|
||||
await page.route("**/api/v1/quick-access/effective*", async (route) => {
|
||||
await route.fulfill({
|
||||
|
||||
Generated
+5
-5
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@govoplan/core-webui",
|
||||
"version": "0.1.18",
|
||||
"version": "0.1.21",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@govoplan/core-webui",
|
||||
"version": "0.1.18",
|
||||
"version": "0.1.21",
|
||||
"dependencies": {
|
||||
"@govoplan/access-webui": "file:../../govoplan-access/webui",
|
||||
"@govoplan/addresses-webui": "file:../../govoplan-addresses/webui",
|
||||
@@ -82,7 +82,7 @@
|
||||
},
|
||||
"../../govoplan-access/webui": {
|
||||
"name": "@govoplan/access-webui",
|
||||
"version": "0.1.18",
|
||||
"version": "0.1.19",
|
||||
"devDependencies": {
|
||||
"typescript": "^5.7.2"
|
||||
},
|
||||
@@ -288,7 +288,7 @@
|
||||
},
|
||||
"../../govoplan-datasources/webui": {
|
||||
"name": "@govoplan/datasources-webui",
|
||||
"version": "0.1.18",
|
||||
"version": "0.1.20",
|
||||
"peerDependencies": {
|
||||
"@govoplan/core-webui": "^0.1.18",
|
||||
"lucide-react": "^1.23.0",
|
||||
@@ -547,7 +547,7 @@
|
||||
},
|
||||
"../../govoplan-policy/webui": {
|
||||
"name": "@govoplan/policy-webui",
|
||||
"version": "0.1.18",
|
||||
"version": "0.1.19",
|
||||
"peerDependencies": {
|
||||
"@govoplan/core-webui": "^0.1.18",
|
||||
"lucide-react": "^1.23.0",
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"name": "@govoplan/core-webui",
|
||||
"version": "0.1.18",
|
||||
"version": "0.1.21",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@govoplan/core-webui",
|
||||
"version": "0.1.18",
|
||||
"version": "0.1.21",
|
||||
"dependencies": {
|
||||
"@govoplan/access-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-access.git#v0.1.18",
|
||||
"@govoplan/access-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-access.git#v0.1.19",
|
||||
"@govoplan/admin-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-admin.git#v0.1.18",
|
||||
"@govoplan/audit-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-audit.git#v0.1.18",
|
||||
"@govoplan/calendar-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-calendar.git#v0.1.18",
|
||||
@@ -753,8 +753,8 @@
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@govoplan/access-webui": {
|
||||
"version": "0.1.18",
|
||||
"resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-access.git#44799b15e56cb1ace679689783be51aac7bee4c3",
|
||||
"version": "0.1.19",
|
||||
"resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-access.git#2d1b1e356ecb8726219d4a502db78e61f435a88f",
|
||||
"peerDependencies": {
|
||||
"@govoplan/core-webui": "^0.1.18",
|
||||
"lucide-react": "^1.23.0",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@govoplan/core-webui",
|
||||
"version": "0.1.18",
|
||||
"version": "0.1.21",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@govoplan/core-webui",
|
||||
"version": "0.1.18",
|
||||
"version": "0.1.21",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
@@ -26,7 +26,7 @@
|
||||
"preview": "vite preview --host 127.0.0.1 --port 4173"
|
||||
},
|
||||
"dependencies": {
|
||||
"@govoplan/access-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-access.git#v0.1.18",
|
||||
"@govoplan/access-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-access.git#v0.1.19",
|
||||
"@govoplan/admin-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-admin.git#v0.1.18",
|
||||
"@govoplan/audit-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-audit.git#v0.1.18",
|
||||
"@govoplan/calendar-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-calendar.git#v0.1.18",
|
||||
|
||||
@@ -26,6 +26,7 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
"i18n:govoplan-core.restore_safe_defaults": "Restore safe defaults",
|
||||
"i18n:govoplan-core.import_overrides": "Import JSON",
|
||||
"i18n:govoplan-core.export_overrides": "Export JSON",
|
||||
"i18n:govoplan-core.explanation_subject.6ff46d22": "Explanation subject",
|
||||
"i18n:govoplan-core.remove_overrides": "Remove overrides",
|
||||
"i18n:govoplan-core.appearance_override_not_configured_help": "No personal token overrides are stored. Preset and policy inheritance remain active.",
|
||||
"i18n:govoplan-core.appearance_override_policy_disabled": "The governing appearance policy currently blocks creating or editing personal overrides. Existing overrides may still be exported or removed.",
|
||||
@@ -761,6 +762,7 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
"i18n:govoplan-core.restore_safe_defaults": "Sichere Standardwerte wiederherstellen",
|
||||
"i18n:govoplan-core.import_overrides": "JSON importieren",
|
||||
"i18n:govoplan-core.export_overrides": "JSON exportieren",
|
||||
"i18n:govoplan-core.explanation_subject.6ff46d22": "Person für die Zugriffserklärung",
|
||||
"i18n:govoplan-core.remove_overrides": "Anpassungen entfernen",
|
||||
"i18n:govoplan-core.appearance_override_not_configured_help": "Es sind keine persönlichen Token-Anpassungen gespeichert. Vorgaben und Richtlinienvererbung bleiben aktiv.",
|
||||
"i18n:govoplan-core.appearance_override_policy_disabled": "Die geltende Darstellungsrichtlinie sperrt derzeit das Anlegen oder Bearbeiten persönlicher Anpassungen. Bestehende Anpassungen können weiterhin exportiert oder entfernt werden.",
|
||||
@@ -936,7 +938,7 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
"i18n:govoplan-core.choose_date.e7877fd9": "Choose date",
|
||||
"i18n:govoplan-core.clear_filter.b667d6f9": "Clear filter",
|
||||
"i18n:govoplan-core.close_filter.3a281c3f": "Close filter",
|
||||
"i18n:govoplan-core.close.bbfa773e": "Close",
|
||||
"i18n:govoplan-core.close.bbfa773e": "Schließen",
|
||||
"i18n:govoplan-core.collapse.9cf188d3": "Collapse",
|
||||
"i18n:govoplan-core.color_picker.91dee962": "Color picker",
|
||||
"i18n:govoplan-core.comfortable.2313707a": "Comfortable",
|
||||
@@ -950,7 +952,7 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
"i18n:govoplan-core.connection_security_mode_expected_by_the_server_.4348e12e": "Connection security mode expected by the server: plain, TLS or STARTTLS.",
|
||||
"i18n:govoplan-core.connection_successful_the_backend_health_endpoin.14eb9295": "Connection successful. The backend health endpoint responded.",
|
||||
"i18n:govoplan-core.contains.33e15d00": "Contains",
|
||||
"i18n:govoplan-core.context_help.7fc44ea1": "Context help",
|
||||
"i18n:govoplan-core.context_help.7fc44ea1": "Kontexthilfe",
|
||||
"i18n:govoplan-core.controls_contextual_ui_help_markers_once_persist.0eaef9c4": "Controls contextual UI help markers once persisted user preferences are available.",
|
||||
"i18n:govoplan-core.core_only.d46ce7d9": "Core only",
|
||||
"i18n:govoplan-core.core_shell_only.4949b5d5": "Core shell only",
|
||||
@@ -1047,9 +1049,9 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
"i18n:govoplan-core.group.171a0606": "Group",
|
||||
"i18n:govoplan-core.groups": "Gruppen",
|
||||
"i18n:govoplan-core.headers.520de744": "Headers",
|
||||
"i18n:govoplan-core.help_context.61aed3b9": "Help context:",
|
||||
"i18n:govoplan-core.help_context.61aed3b9": "Hilfekontext:",
|
||||
"i18n:govoplan-core.help_f1": "Hilfe (F1)",
|
||||
"i18n:govoplan-core.help.c47ae153": "Help",
|
||||
"i18n:govoplan-core.help.c47ae153": "Hilfe",
|
||||
"i18n:govoplan-core.hh_mm.a4c7ee9b": "HH:MM",
|
||||
"i18n:govoplan-core.hide_password.e40123b4": "Hide password",
|
||||
"i18n:govoplan-core.high_level_campaign_pattern_used_by_guided_setup.c5ec6652": "High-level campaign pattern used by guided setup to prefill sensible defaults.",
|
||||
@@ -1174,9 +1176,9 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
"i18n:govoplan-core.on.e0049a66": "On",
|
||||
"i18n:govoplan-core.only_users_with_maintenance_access_can_use_the_s.5566264f": "Only users with maintenance access can use the system right now.",
|
||||
"i18n:govoplan-core.open_address_form.f8ee560f": "Open address form",
|
||||
"i18n:govoplan-core.open_admin_documentation.6adbdae3": "Open admin documentation",
|
||||
"i18n:govoplan-core.open_admin_documentation.6adbdae3": "Administrator-Dokumentation öffnen",
|
||||
"i18n:govoplan-core.open_maintenance_mode_settings.99b41249": "Open maintenance mode settings",
|
||||
"i18n:govoplan-core.open_user_documentation.084af515": "Open user documentation",
|
||||
"i18n:govoplan-core.open_user_documentation.084af515": "Benutzerdokumentation öffnen",
|
||||
"i18n:govoplan-core.optional_backend_url_leave_empty_to_use_the_curr.52bd2bb4": "Optional backend URL. Leave empty to use the current origin and the configured Vite/backend proxy.",
|
||||
"i18n:govoplan-core.optional_html_version_of_the_email_body_for_rich.3f01db97": "Optional HTML version of the email body for richer formatting.",
|
||||
"i18n:govoplan-core.optional_local_alias_for_the_active_tenant_it_do.edbb7b14": "Optional local alias for the active tenant. It does not replace the global account name in the title bar.",
|
||||
@@ -1184,7 +1186,7 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
"i18n:govoplan-core.owner.89ff3122": "Eigentümer",
|
||||
"i18n:govoplan-core.owner_policy.1e8df143": "Owner policy",
|
||||
"i18n:govoplan-core.p_no_html_body_content_p.c305bfc6": "<p>No HTML body content.</p>",
|
||||
"i18n:govoplan-core.page.fb06270f": "Page",
|
||||
"i18n:govoplan-core.page.fb06270f": "Seite",
|
||||
"i18n:govoplan-core.password_for_the_selected_account_or_mail_server.d9681f16": "Password for the selected account or mail server. Stored and transmitted according to the configured backend flow.",
|
||||
"i18n:govoplan-core.password_protected.09d9e174": "Password protected",
|
||||
"i18n:govoplan-core.password.8be3c943": "Passwort",
|
||||
@@ -1346,12 +1348,12 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
"i18n:govoplan-core.theme_and_language.60a8cf59": "Design und Sprache",
|
||||
"i18n:govoplan-core.theme.a797e309": "Theme",
|
||||
"i18n:govoplan-core.this_address_is_already_listed.0f6d0ff2": "This address is already listed.",
|
||||
"i18n:govoplan-core.this_area_is_prepared_for_context_sensitive_help.57665877": "This area is prepared for context-sensitive help. Future help content can use this context identifier, or the equivalent help query parameter",
|
||||
"i18n:govoplan-core.this_area_is_prepared_for_context_sensitive_help.57665877": "Das fokussierte Bedienelement verweist auf den folgenden dokumentierten Hilfekontext:",
|
||||
"i18n:govoplan-core.this_global_name_is_shown_in_the_title_bar_and_f.1d46240e": "This global name is shown in the title bar and follows you across tenant memberships.",
|
||||
"i18n:govoplan-core.this_module_is_prepared_but_not_implemented_yet.6e6449ee": "Dieses Modul ist vorbereitet, aber noch nicht umgesetzt.",
|
||||
"i18n:govoplan-core.this_page_has_unsaved_changes_save_them_before_l.419a9d8b": "This page has unsaved changes. Save them before leaving, or discard the changes and continue.",
|
||||
"i18n:govoplan-core.timeout_seconds.0bfc6553": "Timeout seconds",
|
||||
"i18n:govoplan-core.to_open_the_right_page_or_section.5ecf4fd2": "to open the right page or section.",
|
||||
"i18n:govoplan-core.to_open_the_right_page_or_section.5ecf4fd2": "Öffnen Sie die verknüpfte Dokumentation für Berechtigungen, Folgen und Hinweise zur Wiederherstellung.",
|
||||
"i18n:govoplan-core.to.ae79ea1e": "To",
|
||||
"i18n:govoplan-core.tu.b44892b7": "Tu",
|
||||
"i18n:govoplan-core.type_a_name_and_email_address_then_press_enter.7c8d43f0": "Type a name and email address, then press Enter",
|
||||
|
||||
@@ -8,6 +8,7 @@ export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
"@govoplan/core-webui": resolve(import.meta.dirname, "conformance/QuickAccessCoreFacade.ts"),
|
||||
"virtual:govoplan-installed-modules": resolve(import.meta.dirname, "conformance/installedModules.ts"),
|
||||
react: resolve(import.meta.dirname, "node_modules/react"),
|
||||
"react-router": resolve(import.meta.dirname, "node_modules/react-router"),
|
||||
"lucide-react": resolve(import.meta.dirname, "node_modules/lucide-react")
|
||||
|
||||
Reference in New Issue
Block a user