Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34211e848b | ||
|
|
1236b861b8 | ||
|
|
cd78800a65 |
@@ -104,3 +104,21 @@ From the core checkout, labels can be synced once a local `GITEA_TOKEN` is avail
|
|||||||
cd /mnt/DATA/git/govoplan-core
|
cd /mnt/DATA/git/govoplan-core
|
||||||
/mnt/DATA/git/govoplan/tools/gitea/gitea-sync-labels.py --root /mnt/DATA/git/govoplan-forms-runtime --apply
|
/mnt/DATA/git/govoplan/tools/gitea/gitea-sync-labels.py --root /mnt/DATA/git/govoplan-forms-runtime --apply
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Git-source WebUI package
|
||||||
|
|
||||||
|
The repository root exposes `@govoplan/forms-runtime-webui` for Git-tagged release
|
||||||
|
dependencies. It mirrors the owning `webui/package.json` version, public
|
||||||
|
TypeScript/CSS exports and peer requirements, with entry paths under
|
||||||
|
`webui/src`. Consumers provide the shared Core/React peers; the facade runs no
|
||||||
|
development or install scripts. The source archive contains `webui/src`, this
|
||||||
|
README and any repository license file. Run module development checks from `webui/`; Python
|
||||||
|
installation remains governed by `pyproject.toml`.
|
||||||
|
|
||||||
|
Das Repository stellt `@govoplan/forms-runtime-webui` am Wurzelpfad für versionierte
|
||||||
|
Git-Abhängigkeiten bereit. Version, öffentliche TypeScript-/CSS-Exporte und
|
||||||
|
Peer-Anforderungen entsprechen `webui/package.json`; die Einstiegspfade liegen
|
||||||
|
unter `webui/src`. Gemeinsame Core-/React-Peers stellt die einbindende Anwendung
|
||||||
|
bereit. Die Fassade führt keine Entwicklungs- oder Installationsskripte aus.
|
||||||
|
Entwicklungsprüfungen bleiben in `webui/`, die Python-Installation weiterhin in
|
||||||
|
`pyproject.toml` definiert.
|
||||||
|
|||||||
+29
-3
@@ -1,8 +1,34 @@
|
|||||||
{
|
{
|
||||||
"name": "@govoplan/forms-runtime",
|
"name": "@govoplan/forms-runtime-webui",
|
||||||
"version": "0.1.20",
|
"version": "0.1.22",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Definition-aware form submissions and service launch for GovOPlaN.",
|
"description": "Definition-aware form submissions and service launch for GovOPlaN.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"peerDependencies": {}
|
"peerDependencies": {
|
||||||
|
"@govoplan/core-webui": "^0.1.18",
|
||||||
|
"lucide-react": "^1.23.0",
|
||||||
|
"react": ">=19.2.7 <20",
|
||||||
|
"react-dom": ">=19.2.7 <20",
|
||||||
|
"react-router": ">=8.3.0 <9"
|
||||||
|
},
|
||||||
|
"main": "webui/src/index.ts",
|
||||||
|
"module": "webui/src/index.ts",
|
||||||
|
"types": "webui/src/index.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./webui/src/index.ts",
|
||||||
|
"import": "./webui/src/index.ts"
|
||||||
|
},
|
||||||
|
"./styles/forms-runtime.css": "./webui/src/styles/forms-runtime.css"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@govoplan/core-webui": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"webui/src",
|
||||||
|
"README.md",
|
||||||
|
"LICENSE"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "govoplan-forms-runtime"
|
name = "govoplan-forms-runtime"
|
||||||
version = "0.1.20"
|
version = "0.1.22"
|
||||||
description = "Definition-aware form submissions and service launch for GovOPlaN."
|
description = "Definition-aware form submissions and service launch for GovOPlaN."
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ from govoplan_core.core.dsar import (
|
|||||||
DsarSubjectRef,
|
DsarSubjectRef,
|
||||||
dsar_capability_name,
|
dsar_capability_name,
|
||||||
)
|
)
|
||||||
|
from govoplan_forms_runtime.backend.email_normalization import normalize_status_email as _normalized_email
|
||||||
from govoplan_forms_runtime.backend.db.models import (
|
from govoplan_forms_runtime.backend.db.models import (
|
||||||
FormAcknowledgement,
|
FormAcknowledgement,
|
||||||
FormAssistedConfirmation,
|
FormAssistedConfirmation,
|
||||||
@@ -1116,21 +1117,6 @@ def _session(value: object) -> Session:
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
def _normalized_email(value: object) -> str | None:
|
|
||||||
candidate = str(value or "").strip().casefold()
|
|
||||||
if (
|
|
||||||
not candidate
|
|
||||||
or len(candidate) > 320
|
|
||||||
or candidate.count("@") != 1
|
|
||||||
or any(character.isspace() for character in candidate)
|
|
||||||
):
|
|
||||||
return None
|
|
||||||
local, domain = candidate.rsplit("@", 1)
|
|
||||||
if not local or "." not in domain or domain.startswith(".") or domain.endswith("."):
|
|
||||||
return None
|
|
||||||
return candidate
|
|
||||||
|
|
||||||
|
|
||||||
def _normalized_id(value: object) -> str | None:
|
def _normalized_id(value: object) -> str | None:
|
||||||
if value is None:
|
if value is None:
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
"""Existing Forms Runtime email-selector policy, shared by status access and DSAR.
|
||||||
|
|
||||||
|
This is selector canonicalization, not a transformation of submitted form values
|
||||||
|
or a general-purpose email-address validator.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_status_email(value: object) -> str | None:
|
||||||
|
candidate = str(value or "").strip().casefold()
|
||||||
|
if (
|
||||||
|
not candidate
|
||||||
|
or len(candidate) > 320
|
||||||
|
or candidate.count("@") != 1
|
||||||
|
or any(character.isspace() for character in candidate)
|
||||||
|
):
|
||||||
|
return None
|
||||||
|
local, domain = candidate.rsplit("@", 1)
|
||||||
|
if not local or "." not in domain or domain.startswith(".") or domain.endswith("."):
|
||||||
|
return None
|
||||||
|
return candidate
|
||||||
@@ -65,7 +65,7 @@ from govoplan_forms_runtime.backend.status_access import FormStatusAccessService
|
|||||||
|
|
||||||
MODULE_ID = "forms_runtime"
|
MODULE_ID = "forms_runtime"
|
||||||
MODULE_NAME = "Forms Runtime"
|
MODULE_NAME = "Forms Runtime"
|
||||||
MODULE_VERSION = "0.1.20"
|
MODULE_VERSION = "0.1.22"
|
||||||
PARTICIPATE_SCOPE = "forms_runtime:submission:participate"
|
PARTICIPATE_SCOPE = "forms_runtime:submission:participate"
|
||||||
ASSIST_SCOPE = "forms_runtime:submission:assist"
|
ASSIST_SCOPE = "forms_runtime:submission:assist"
|
||||||
READ_SCOPE = "forms_runtime:workspace:read"
|
READ_SCOPE = "forms_runtime:workspace:read"
|
||||||
@@ -568,6 +568,8 @@ manifest = ModuleManifest(
|
|||||||
title="Complete and manage Forms",
|
title="Complete and manage Forms",
|
||||||
summary="Save permitted drafts, submit validated values, and retain exact definition and handoff evidence.",
|
summary="Save permitted drafts, submit validated values, and retain exact definition and handoff evidence.",
|
||||||
body=(
|
body=(
|
||||||
|
"Documentation books sit beside Forms runtime or Form instance. Editing guidance is beside "
|
||||||
|
"the displayed form title while the instance is editable; field help stays with its label. "
|
||||||
"Every instance resolves one immutable published Form revision. Draft and final values are validated on the server; final submission also enforces attachment, signature, and policy requirements. "
|
"Every instance resolves one immutable published Form revision. Draft and final values are validated on the server; final submission also enforces attachment, signature, and policy requirements. "
|
||||||
"Service launches retain the exact Service and binding. Native Case and Workflow handoffs persist intent before execution, use owner capabilities with stable provider keys, and require reconciliation after unknown outcomes. History, receipts, and handoffs are replay-safe and optimistic-concurrency guarded."
|
"Service launches retain the exact Service and binding. Native Case and Workflow handoffs persist intent before execution, use owner capabilities with stable provider keys, and require reconciliation after unknown outcomes. History, receipts, and handoffs are replay-safe and optimistic-concurrency guarded."
|
||||||
" Invitation and explicitly enabled anonymous intake use hash-only expiring tokens, bounded rate limits, and isolated synthetic actors. Authenticated assisted sessions retain channel, affected and represented parties, authority, purpose, notice, responsible function, language, accessibility needs, and field provenance without bypassing the exact Form rules. Submission requires immutable read-back evidence bound to the current revision, values, attachments, and signatures; any later draft edit invalidates it. Administrators can configure applicant status per exact Form revision as authenticated-only, a short-lived link sent to a matching Form email, or a non-expiring public link. Status projections expose only a bounded lifecycle timeline and receipt reference, never Form values, actors, evidence, internal notes, or handoff details. Files-backed attachments use one-time purpose-bound grants, while authenticated acknowledgements bind an exact actor and payload digest without claiming advanced or qualified signature assurance. When Search is enabled, Forms Runtime contributes a rebuildable metadata-only projection; submitted values and evidence content are excluded, and every candidate receives a current workspace or participant access check."
|
" Invitation and explicitly enabled anonymous intake use hash-only expiring tokens, bounded rate limits, and isolated synthetic actors. Authenticated assisted sessions retain channel, affected and represented parties, authority, purpose, notice, responsible function, language, accessibility needs, and field provenance without bypassing the exact Form rules. Submission requires immutable read-back evidence bound to the current revision, values, attachments, and signatures; any later draft edit invalidates it. Administrators can configure applicant status per exact Form revision as authenticated-only, a short-lived link sent to a matching Form email, or a non-expiring public link. Status projections expose only a bounded lifecycle timeline and receipt reference, never Form values, actors, evidence, internal notes, or handoff details. Files-backed attachments use one-time purpose-bound grants, while authenticated acknowledgements bind an exact actor and payload digest without claiming advanced or qualified signature assurance. When Search is enabled, Forms Runtime contributes a rebuildable metadata-only projection; submitted values and evidence content are excluded, and every candidate receives a current workspace or participant access check."
|
||||||
@@ -625,6 +627,9 @@ manifest = ModuleManifest(
|
|||||||
"Zulässige Entwürfe speichern, validierte Werte einreichen und exakte Definitions- und Übergabenachweise bewahren."
|
"Zulässige Entwürfe speichern, validierte Werte einreichen und exakte Definitions- und Übergabenachweise bewahren."
|
||||||
),
|
),
|
||||||
"body": (
|
"body": (
|
||||||
|
"Dokumentationsbücher stehen neben Formularlaufzeit oder Formularinstanz. Solange die Instanz "
|
||||||
|
"bearbeitbar ist, steht die Bearbeitungshilfe neben dem angezeigten Formulartitel; Feldhilfe "
|
||||||
|
"bleibt bei der Feldbezeichnung. "
|
||||||
"Jede Instanz löst genau eine unveränderliche veröffentlichte Formularrevision auf. Entwurfs- und Endwerte "
|
"Jede Instanz löst genau eine unveränderliche veröffentlichte Formularrevision auf. Entwurfs- und Endwerte "
|
||||||
"werden auf dem Server validiert; die endgültige Einreichung erzwingt zusätzlich Anforderungen an Anlagen, "
|
"werden auf dem Server validiert; die endgültige Einreichung erzwingt zusätzlich Anforderungen an Anlagen, "
|
||||||
"Signaturen und Regeln. Service-Aufrufe bewahren den exakten Service und seine Bindung. Native Übergaben an "
|
"Signaturen und Regeln. Service-Aufrufe bewahren den exakten Service und seine Bindung. Native Übergaben an "
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ from govoplan_forms_runtime.backend.db.models import (
|
|||||||
FormStatusAccessToken,
|
FormStatusAccessToken,
|
||||||
)
|
)
|
||||||
from govoplan_forms_runtime.backend.domain import FormInstance
|
from govoplan_forms_runtime.backend.domain import FormInstance
|
||||||
|
from govoplan_forms_runtime.backend.email_normalization import normalize_status_email as _normalize_email
|
||||||
|
|
||||||
|
|
||||||
STATUS_ACCESS_MODES = frozenset(
|
STATUS_ACCESS_MODES = frozenset(
|
||||||
@@ -616,21 +617,6 @@ def _consume_request_limit(grant: FormStatusAccessGrant, *, now: datetime) -> bo
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def _normalize_email(value: object) -> str | None:
|
|
||||||
candidate = str(value or "").strip().casefold()
|
|
||||||
if (
|
|
||||||
not candidate
|
|
||||||
or len(candidate) > 320
|
|
||||||
or candidate.count("@") != 1
|
|
||||||
or any(character.isspace() for character in candidate)
|
|
||||||
):
|
|
||||||
return None
|
|
||||||
local, domain = candidate.rsplit("@", 1)
|
|
||||||
if not local or "." not in domain or domain.startswith(".") or domain.endswith("."):
|
|
||||||
return None
|
|
||||||
return candidate
|
|
||||||
|
|
||||||
|
|
||||||
def _email_digest(grant_id: str, email: str) -> str:
|
def _email_digest(grant_id: str, email: str) -> str:
|
||||||
return hashlib.sha256(f"{grant_id}\0{email}".encode("utf-8")).hexdigest()
|
return hashlib.sha256(f"{grant_id}\0{email}".encode("utf-8")).hexdigest()
|
||||||
|
|
||||||
|
|||||||
Executable
+37
@@ -0,0 +1,37 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
from govoplan_forms_runtime.backend.dsar_provider import _normalized_email
|
||||||
|
from govoplan_forms_runtime.backend.email_normalization import normalize_status_email
|
||||||
|
from govoplan_forms_runtime.backend.status_access import _normalize_email
|
||||||
|
|
||||||
|
|
||||||
|
class EmailNormalizationTests(unittest.TestCase):
|
||||||
|
def test_status_access_and_dsar_share_the_same_selector_policy(self) -> None:
|
||||||
|
self.assertIs(normalize_status_email, _normalize_email)
|
||||||
|
self.assertIs(normalize_status_email, _normalized_email)
|
||||||
|
|
||||||
|
def test_existing_selector_results_remain_exact_at_policy_boundaries(self) -> None:
|
||||||
|
boundary = "a" * 308 + "@example.org"
|
||||||
|
values = (
|
||||||
|
(None, None), (False, None), (0, None), ("", None), (" ", None),
|
||||||
|
(" Subject@Example.ORG ", "subject@example.org"),
|
||||||
|
("\u00a0Straße@BÜRO.Example\u00a0", "strasse@büro.example"),
|
||||||
|
("a+b@sub.example.org", "a+b@sub.example.org"),
|
||||||
|
("a..b@example..org", "a..b@example..org"), # Existing policy, not a new RFC validator.
|
||||||
|
("a b@example.org", None), ("a@exa\tmple.org", None), ("a@exa\u00a0mple.org", None),
|
||||||
|
("a@@example.org", None), ("@example.org", None), ("a@example", None),
|
||||||
|
("a@.example.org", None), ("a@example.org.", None),
|
||||||
|
(boundary, boundary), ("a" + boundary, None),
|
||||||
|
)
|
||||||
|
for value, expected in values:
|
||||||
|
with self.subTest(value=value):
|
||||||
|
self.assertEqual(expected, normalize_status_email(value))
|
||||||
|
|
||||||
|
def test_selector_canonicalization_does_not_mutate_submitted_values(self) -> None:
|
||||||
|
values = {"email": " Subject@Example.ORG ", "other": ["001", " ", None]}
|
||||||
|
self.assertEqual("subject@example.org", normalize_status_email(values["email"]))
|
||||||
|
self.assertEqual({"email": " Subject@Example.ORG ", "other": ["001", " ", None]}, values)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@govoplan/forms-runtime-webui",
|
"name": "@govoplan/forms-runtime-webui",
|
||||||
"version": "0.1.20",
|
"version": "0.1.22",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { ActionBlockerHint,
|
|||||||
DescriptionList,
|
DescriptionList,
|
||||||
Dialog,
|
Dialog,
|
||||||
DocumentationHelpLink,
|
DocumentationHelpLink,
|
||||||
|
TextWithHelp,
|
||||||
DismissibleAlert,
|
DismissibleAlert,
|
||||||
FileDropZone,
|
FileDropZone,
|
||||||
FormField as CoreFormField,
|
FormField as CoreFormField,
|
||||||
@@ -397,6 +398,8 @@ export default function FormInstancePage({ settings, auth }: PlatformRouteContex
|
|||||||
<main className="forms-runtime-page">
|
<main className="forms-runtime-page">
|
||||||
<WorkspaceFrame className="form-instance-shell" label="Form instance" interfaceId="forms-runtime.instance" helpContextId="forms-runtime.page.instance" helpModuleId="forms-runtime">
|
<WorkspaceFrame className="form-instance-shell" label="Form instance" interfaceId="forms-runtime.instance" helpContextId="forms-runtime.page.instance" helpModuleId="forms-runtime">
|
||||||
<WorkspaceActionBar
|
<WorkspaceActionBar
|
||||||
|
title="Form instance"
|
||||||
|
titleHelp={<DocumentationHelpLink reference={FORMS_RUNTIME_DOCUMENTATION} />}
|
||||||
scope="workspace"
|
scope="workspace"
|
||||||
variant="detail"
|
variant="detail"
|
||||||
refreshable
|
refreshable
|
||||||
@@ -416,7 +419,6 @@ export default function FormInstancePage({ settings, auth }: PlatformRouteContex
|
|||||||
File in eAkte
|
File in eAkte
|
||||||
</Button>
|
</Button>
|
||||||
: undefined}
|
: undefined}
|
||||||
helpAction={<DocumentationHelpLink reference={FORMS_RUNTIME_DOCUMENTATION} />}
|
|
||||||
/>
|
/>
|
||||||
<PageScrollViewport className="form-instance-viewport">
|
<PageScrollViewport className="form-instance-viewport">
|
||||||
{error &&
|
{error &&
|
||||||
@@ -446,7 +448,7 @@ export default function FormInstancePage({ settings, auth }: PlatformRouteContex
|
|||||||
<div className="form-instance-content">
|
<div className="form-instance-content">
|
||||||
<section className="form-instance-main">
|
<section className="form-instance-main">
|
||||||
<header>
|
<header>
|
||||||
<h1>{localized.title}</h1>
|
<TextWithHelp as="div" help={editable ? <DocumentationHelpLink reference={FORMS_RUNTIME_FIELD_DOCUMENTATION} /> : undefined}><h1>{localized.title}</h1></TextWithHelp>
|
||||||
{localized.description && <p>{localized.description}</p>}
|
{localized.description && <p>{localized.description}</p>}
|
||||||
</header>
|
</header>
|
||||||
{assisted &&
|
{assisted &&
|
||||||
@@ -553,7 +555,6 @@ export default function FormInstancePage({ settings, auth }: PlatformRouteContex
|
|||||||
}
|
}
|
||||||
{editable &&
|
{editable &&
|
||||||
<div className="form-instance-actions">
|
<div className="form-instance-actions">
|
||||||
<DocumentationHelpLink reference={FORMS_RUNTIME_FIELD_DOCUMENTATION} />
|
|
||||||
{canSave &&
|
{canSave &&
|
||||||
<label className="form-change-reason">
|
<label className="form-change-reason">
|
||||||
<span>Change reason</span>
|
<span>Change reason</span>
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ export default function FormsRuntimePage({ settings, auth }: PlatformRouteContex
|
|||||||
<main className="forms-runtime-page">
|
<main className="forms-runtime-page">
|
||||||
<WorkspaceFrame className="forms-runtime-shell" label="Forms runtime" interfaceId="forms-runtime.workspace" helpContextId="forms-runtime.page.workspace" helpModuleId="forms-runtime">
|
<WorkspaceFrame className="forms-runtime-shell" label="Forms runtime" interfaceId="forms-runtime.workspace" helpContextId="forms-runtime.page.workspace" helpModuleId="forms-runtime">
|
||||||
<WorkspaceActionBar
|
<WorkspaceActionBar
|
||||||
|
title="Forms runtime"
|
||||||
|
titleHelp={<DocumentationHelpLink reference={FORMS_RUNTIME_DOCUMENTATION} />}
|
||||||
scope="workspace"
|
scope="workspace"
|
||||||
variant="collection"
|
variant="collection"
|
||||||
refreshable
|
refreshable
|
||||||
@@ -110,7 +112,6 @@ export default function FormsRuntimePage({ settings, auth }: PlatformRouteContex
|
|||||||
Assisted intake
|
Assisted intake
|
||||||
</Button>
|
</Button>
|
||||||
: undefined}
|
: undefined}
|
||||||
helpAction={<DocumentationHelpLink reference={FORMS_RUNTIME_DOCUMENTATION} />}
|
|
||||||
/>
|
/>
|
||||||
<PageScrollViewport className="forms-runtime-list-viewport">
|
<PageScrollViewport className="forms-runtime-list-viewport">
|
||||||
{error &&
|
{error &&
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import type { PlatformTranslations } from "@govoplan/core-webui";
|
import type { PlatformTranslations } from "@govoplan/core-webui";
|
||||||
|
|
||||||
const en = {
|
const en = {
|
||||||
|
"Form instance": "Form instance",
|
||||||
|
"Forms runtime": "Forms runtime",
|
||||||
"i18n:govoplan-forms-runtime.forms": "Forms",
|
"i18n:govoplan-forms-runtime.forms": "Forms",
|
||||||
"i18n:govoplan-forms-runtime.loading_reason": "Form data is still loading.",
|
"i18n:govoplan-forms-runtime.loading_reason": "Form data is still loading.",
|
||||||
"i18n:govoplan-forms-runtime.saving_reason": "A Form operation is still running.",
|
"i18n:govoplan-forms-runtime.saving_reason": "A Form operation is still running.",
|
||||||
@@ -202,6 +204,8 @@ const en = {
|
|||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const de: Record<keyof typeof en, string> = {
|
const de: Record<keyof typeof en, string> = {
|
||||||
|
"Form instance": "Formularinstanz",
|
||||||
|
"Forms runtime": "Formularlaufzeit",
|
||||||
"i18n:govoplan-forms-runtime.forms": "Formulare",
|
"i18n:govoplan-forms-runtime.forms": "Formulare",
|
||||||
"i18n:govoplan-forms-runtime.loading_reason": "Formulardaten werden noch geladen.",
|
"i18n:govoplan-forms-runtime.loading_reason": "Formulardaten werden noch geladen.",
|
||||||
"i18n:govoplan-forms-runtime.saving_reason": "Eine Formularaktion läuft noch.",
|
"i18n:govoplan-forms-runtime.saving_reason": "Eine Formularaktion läuft noch.",
|
||||||
|
|||||||
Reference in New Issue
Block a user