Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34211e848b | ||
|
|
1236b861b8 | ||
|
|
cd78800a65 | ||
|
|
29470d9a6e |
@@ -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.
|
||||||
|
|||||||
@@ -62,6 +62,10 @@ Runtime form submissions for validation, drafts, attachments, signatures, status
|
|||||||
channel, affected and represented party references, authority basis, purpose,
|
channel, affected and represented party references, authority basis, purpose,
|
||||||
notice, responsible function, language, accessibility needs, and field-level
|
notice, responsible function, language, accessibility needs, and field-level
|
||||||
source/confidence provenance on the same exact Form revision used digitally
|
source/confidence provenance on the same exact Form revision used digitally
|
||||||
|
- a responsive operator source editor that classifies each populated field
|
||||||
|
independently, including its confidence and optional governed declaring-party,
|
||||||
|
document, or source-system reference; it restores the current revision's
|
||||||
|
retained source map instead of flattening mixed provenance
|
||||||
- append-only assisted read-back outcomes bound to the exact current Form
|
- append-only assisted read-back outcomes bound to the exact current Form
|
||||||
revision, normalized values, attachments, and signatures; draft corrections
|
revision, normalized values, attachments, and signatures; draft corrections
|
||||||
invalidate earlier confirmation evidence and submission fails closed until a
|
invalidate earlier confirmation evidence and submission fails closed until a
|
||||||
@@ -157,6 +161,9 @@ The immutable outcome identifies the confirming party and operator. Corrections
|
|||||||
are saved as a new Form revision and require a new read-back. When confirmation
|
are saved as a new Form revision and require a new read-back. When confirmation
|
||||||
cannot be obtained, the operator must choose that outcome and record the
|
cannot be obtained, the operator must choose that outcome and record the
|
||||||
exception; policy or downstream review may still stop or escalate the case.
|
exception; policy or downstream review may still stop or escalate the case.
|
||||||
|
Newly started assisted sessions may be saved as their first ordinary draft;
|
||||||
|
this is required before read-back whenever the operator entered or corrected
|
||||||
|
values, attachments, or acknowledgements.
|
||||||
|
|
||||||
## Approved Intake And Evidence Profiles
|
## Approved Intake And Evidence Profiles
|
||||||
|
|
||||||
|
|||||||
@@ -30,4 +30,7 @@ instance revisions, validation, receipts, and handoff evidence.
|
|||||||
Native controls preserve keyboard order, shared dialogs manage focus, changed
|
Native controls preserve keyboard order, shared dialogs manage focus, changed
|
||||||
values use the global unsaved-draft guard, and bounded list/detail regions keep
|
values use the global unsaved-draft guard, and bounded list/detail regions keep
|
||||||
their existing responsive scrolling. English and German catalogues cover
|
their existing responsive scrolling. English and German catalogues cover
|
||||||
module-owned copy; definition content uses its own published localization.
|
module-owned copy; definition content uses its own published localization. The
|
||||||
|
assisted read-back dialog uses one responsive fieldset per populated value so
|
||||||
|
mixed person, representative, document, system, and derived provenance remains
|
||||||
|
legible and independently editable at desktop and mobile widths.
|
||||||
|
|||||||
+29
-3
@@ -1,8 +1,34 @@
|
|||||||
{
|
{
|
||||||
"name": "@govoplan/forms-runtime",
|
"name": "@govoplan/forms-runtime-webui",
|
||||||
"version": "0.1.19",
|
"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.19"
|
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.19"
|
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 "
|
||||||
@@ -674,7 +679,7 @@ manifest = ModuleManifest(
|
|||||||
"stable idempotency key. Rejected effects may be retried. Unknown outcomes must be reconciled before retry to avoid a "
|
"stable idempotency key. Rejected effects may be retried. Unknown outcomes must be reconciled before retry to avoid a "
|
||||||
"duplicate target. Administrative compensation records verified absence and never deletes a remote target. "
|
"duplicate target. Administrative compensation records verified absence and never deletes a remote target. "
|
||||||
"When Records is enabled, only immutable submitted revisions can be resolved for filing; current Forms Runtime access is rechecked and editable drafts fail closed."
|
"When Records is enabled, only immutable submitted revisions can be resolved for filing; current Forms Runtime access is rechecked and editable drafts fail closed."
|
||||||
" Assisted intake begins with an authenticated, purpose-bound session. The operator records the channel, party and representation references, authority basis, notice, responsible function, language, accessibility support, and per-field sources. Read-back outcomes are append-only and bind the exact current payload. Corrections must first be saved as a new draft revision and confirmed again; an unavailable confirmation requires an explicit exception note."
|
" Assisted intake begins with an authenticated, purpose-bound session. The operator records the channel, party and representation references, authority basis, notice, responsible function, language, accessibility support, and per-field sources. The operator interface classifies every populated value independently and restores source, confidence, and declaring-party references retained for the current revision. A newly started assisted session can be saved as its first draft before read-back. Read-back outcomes are append-only and bind the exact current payload. Corrections must first be saved as a new draft revision and confirmed again; an unavailable confirmation requires an explicit exception note."
|
||||||
" Applicant status policies are attached to one exact published Form revision. Authenticated access is object-bound to the applicant account; assisted intake can bind it only from an explicit account party reference. Email-link mode compares the submitted email without revealing whether it matched, revokes the earlier link on resend, and delegates delivery to Notifications. Permanent-link mode deliberately trades authentication and expiry for possession of a stable high-entropy URL. Disabling a policy immediately suspends all grants issued under it; changing policy fields affects future submissions while existing grants retain their issued access profile."
|
" Applicant status policies are attached to one exact published Form revision. Authenticated access is object-bound to the applicant account; assisted intake can bind it only from an explicit account party reference. Email-link mode compares the submitted email without revealing whether it matched, revokes the earlier link on resend, and delegates delivery to Notifications. Permanent-link mode deliberately trades authentication and expiry for possession of a stable high-entropy URL. Disabling a policy immediately suspends all grants issued under it; changing policy fields affects future submissions while existing grants retain their issued access profile."
|
||||||
),
|
),
|
||||||
layer="configured",
|
layer="configured",
|
||||||
@@ -745,7 +750,10 @@ manifest = ModuleManifest(
|
|||||||
"unveränderliche eingereichte Revisionen für die Ablage aufgelöst werden; der aktuelle Zugriff in Forms Runtime "
|
"unveränderliche eingereichte Revisionen für die Ablage aufgelöst werden; der aktuelle Zugriff in Forms Runtime "
|
||||||
"wird erneut geprüft und bearbeitbare Entwürfe werden abgewiesen. Assistierte Erfassung beginnt mit einer "
|
"wird erneut geprüft und bearbeitbare Entwürfe werden abgewiesen. Assistierte Erfassung beginnt mit einer "
|
||||||
"authentifizierten zweckgebundenen Sitzung. Bearbeitende erfassen Kanal, Partei- und Vertretungsverweise, "
|
"authentifizierten zweckgebundenen Sitzung. Bearbeitende erfassen Kanal, Partei- und Vertretungsverweise, "
|
||||||
"Befugnisgrundlage, Hinweis, zuständige Funktion, Sprache, Barrierefreiheit und Quellen je Feld. "
|
"Befugnisgrundlage, Hinweis, zuständige Funktion, Sprache, Barrierefreiheit und Quellen je Feld. Die "
|
||||||
|
"Bearbeitungsoberfläche klassifiziert jeden ausgefüllten Wert unabhängig und stellt für die aktuelle Revision "
|
||||||
|
"bewahrte Quellen-, Vertrauens- und Erklärendenreferenzen wieder her. Eine neu begonnene assistierte Sitzung "
|
||||||
|
"kann vor dem Rücklesen als erster Entwurf gespeichert werden. "
|
||||||
"Rückleseergebnisse werden nur angefügt und binden die exakten aktuellen Nutzdaten. Korrekturen müssen zuerst "
|
"Rückleseergebnisse werden nur angefügt und binden die exakten aktuellen Nutzdaten. Korrekturen müssen zuerst "
|
||||||
"als neue Entwurfsrevision gespeichert und erneut bestätigt werden; eine nicht mögliche Bestätigung erfordert "
|
"als neue Entwurfsrevision gespeichert und erneut bestätigt werden; eine nicht mögliche Bestätigung erfordert "
|
||||||
"einen ausdrücklichen Ausnahmevermerk. Regeln für Antragstellerstatus sind an eine exakte veröffentlichte "
|
"einen ausdrücklichen Ausnahmevermerk. Regeln für Antragstellerstatus sind an eine exakte veröffentlichte "
|
||||||
@@ -789,7 +797,6 @@ manifest = ModuleManifest(
|
|||||||
test_ref="tests/test_forms_runtime.py",
|
test_ref="tests/test_forms_runtime.py",
|
||||||
known_limits=(
|
known_limits=(
|
||||||
"External advanced or qualified signature providers, scheduled expiry cleanup, and target kinds beyond native Case/Workflow handoffs remain adapter depth. Public links intentionally cannot substitute the native authenticated acknowledgement profile.",
|
"External advanced or qualified signature providers, scheduled expiry cleanup, and target kinds beyond native Case/Workflow handoffs remain adapter depth. Public links intentionally cannot substitute the native authenticated acknowledgement profile.",
|
||||||
"The assisted-intake API retains per-field provenance, while the first operator dialog applies one selected source/confidence profile to all populated values; mixed-source field editing remains UI depth.",
|
|
||||||
"Status policy field changes apply to future submissions; existing grants retain their issued mode and limits, while disabling the policy suspends all of them immediately. Scheduled token cleanup and per-IP throttling remain operations depth.",
|
"Status policy field changes apply to future submissions; existing grants retain their issued mode and limits, while disabling the policy suspends all of them immediately. Scheduled token cleanup and per-IP throttling remain operations depth.",
|
||||||
),
|
),
|
||||||
supported_authority_modes=("native_authoritative",),
|
supported_authority_modes=("native_authoritative",),
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -38,6 +38,11 @@ class FormsRuntimeInterfaceDocumentationContractTests(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
self.assertIn("submit", reference.metadata["consequence_classes"])
|
self.assertIn("submit", reference.metadata["consequence_classes"])
|
||||||
self.assertIn("compensate", reference.metadata["consequence_classes"])
|
self.assertIn("compensate", reference.metadata["consequence_classes"])
|
||||||
|
self.assertIn("every populated value independently", reference.body)
|
||||||
|
self.assertFalse(any(
|
||||||
|
"mixed-source field editing remains" in limit
|
||||||
|
for limit in manifest.architecture.known_limits
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@govoplan/forms-runtime-webui",
|
"name": "@govoplan/forms-runtime-webui",
|
||||||
"version": "0.1.19",
|
"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,
|
||||||
@@ -89,8 +90,7 @@ export default function FormInstancePage({ settings, auth }: PlatformRouteContex
|
|||||||
const [assistedOutcome, setAssistedOutcome] = useState<AssistedConfirmation["outcome"]>("confirmed");
|
const [assistedOutcome, setAssistedOutcome] = useState<AssistedConfirmation["outcome"]>("confirmed");
|
||||||
const [assistedMethod, setAssistedMethod] = useState<AssistedConfirmation["method"]>("spoken_readback");
|
const [assistedMethod, setAssistedMethod] = useState<AssistedConfirmation["method"]>("spoken_readback");
|
||||||
const [assistedConfirmedBy, setAssistedConfirmedBy] = useState("");
|
const [assistedConfirmedBy, setAssistedConfirmedBy] = useState("");
|
||||||
const [assistedSource, setAssistedSource] = useState<AssistedIntakeContext["field_sources"][string]["source"]>("person_statement");
|
const [assistedFieldSources, setAssistedFieldSources] = useState<AssistedIntakeContext["field_sources"]>({});
|
||||||
const [assistedConfidence, setAssistedConfidence] = useState<AssistedIntakeContext["field_sources"][string]["confidence"]>("stated");
|
|
||||||
const [assistedNote, setAssistedNote] = useState("");
|
const [assistedNote, setAssistedNote] = useState("");
|
||||||
|
|
||||||
const load = useCallback(async (signal?: AbortSignal) => {
|
const load = useCallback(async (signal?: AbortSignal) => {
|
||||||
@@ -115,6 +115,11 @@ export default function FormInstancePage({ settings, auth }: PlatformRouteContex
|
|||||||
setHandoffs(nextHandoffs.handoffs);
|
setHandoffs(nextHandoffs.handoffs);
|
||||||
setAssistedConfirmations(nextConfirmations.confirmations);
|
setAssistedConfirmations(nextConfirmations.confirmations);
|
||||||
setAssistedConfirmedBy((current) => current || assisted?.affected_party_ref || "");
|
setAssistedConfirmedBy((current) => current || assisted?.affected_party_ref || "");
|
||||||
|
setAssistedFieldSources(assistedSourcesForRevision(
|
||||||
|
nextInstance,
|
||||||
|
assisted,
|
||||||
|
nextConfirmations.confirmations
|
||||||
|
));
|
||||||
setValues(nextInstance.values);
|
setValues(nextInstance.values);
|
||||||
setAttachmentRefs(nextInstance.attachment_refs);
|
setAttachmentRefs(nextInstance.attachment_refs);
|
||||||
setSignatureRefs(nextInstance.signature_refs);
|
setSignatureRefs(nextInstance.signature_refs);
|
||||||
@@ -147,7 +152,7 @@ export default function FormInstancePage({ settings, auth }: PlatformRouteContex
|
|||||||
const canAdmin = hasScope(auth, "forms_runtime:workspace:admin");
|
const canAdmin = hasScope(auth, "forms_runtime:workspace:admin");
|
||||||
const canEditPermission = canParticipate || canWrite;
|
const canEditPermission = canParticipate || canWrite;
|
||||||
const editable = Boolean(editableLifecycle && canEditPermission);
|
const editable = Boolean(editableLifecycle && canEditPermission);
|
||||||
const canSave = Boolean(instance?.status === "draft" && definition?.allow_drafts && canEditPermission);
|
const canSave = Boolean(editableLifecycle && definition?.allow_drafts && canEditPermission);
|
||||||
const changed = useMemo(
|
const changed = useMemo(
|
||||||
() => Boolean(instance && (
|
() => Boolean(instance && (
|
||||||
JSON.stringify(values) !== JSON.stringify(instance.values)
|
JSON.stringify(values) !== JSON.stringify(instance.values)
|
||||||
@@ -248,11 +253,9 @@ export default function FormInstancePage({ settings, auth }: PlatformRouteContex
|
|||||||
fieldSources: Object.fromEntries(
|
fieldSources: Object.fromEntries(
|
||||||
Object.entries(values)
|
Object.entries(values)
|
||||||
.filter(([, value]) => value !== null && value !== undefined && value !== "")
|
.filter(([, value]) => value !== null && value !== undefined && value !== "")
|
||||||
.map(([key]) => [key, {
|
.map(([key]) => [key, assistedFieldSources[key] ?? defaultAssistedFieldSource(
|
||||||
source: assistedSource,
|
assistedConfirmedBy.trim() || assisted.affected_party_ref
|
||||||
confidence: assistedConfidence,
|
)])
|
||||||
declared_by_ref: assistedConfirmedBy.trim()
|
|
||||||
}])
|
|
||||||
),
|
),
|
||||||
correctionNote: assistedNote
|
correctionNote: assistedNote
|
||||||
}
|
}
|
||||||
@@ -277,6 +280,21 @@ export default function FormInstancePage({ settings, auth }: PlatformRouteContex
|
|||||||
setSignatureRefs([]);
|
setSignatureRefs([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function changeAssistedFieldSource(
|
||||||
|
fieldKey: string,
|
||||||
|
patch: Partial<AssistedIntakeContext["field_sources"][string]>
|
||||||
|
) {
|
||||||
|
setAssistedFieldSources((current) => ({
|
||||||
|
...current,
|
||||||
|
[fieldKey]: {
|
||||||
|
...(current[fieldKey] ?? defaultAssistedFieldSource(
|
||||||
|
assistedConfirmedBy.trim() || assisted?.affected_party_ref || ""
|
||||||
|
)),
|
||||||
|
...patch
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
async function uploadAttachments(files: File[]) {
|
async function uploadAttachments(files: File[]) {
|
||||||
if (!instance || !definition) return;
|
if (!instance || !definition) return;
|
||||||
const available = Math.max(0, definition.max_attachments - attachmentRefs.length);
|
const available = Math.max(0, definition.max_attachments - attachmentRefs.length);
|
||||||
@@ -380,26 +398,27 @@ 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
|
||||||
reloadAction={{ onReload: () => void load(), loading }}
|
reloadAction={{ onReload: () => void load(), loading }}
|
||||||
className="form-instance-toolbar"
|
className="form-instance-toolbar"
|
||||||
contextActions={<>
|
contextActions={<span className="form-instance-toolbar-context">
|
||||||
<Button onClick={() => navigate("/forms-runtime")}>
|
<Button onClick={() => navigate("/forms-runtime")}>
|
||||||
<ArrowLeft size={16} aria-hidden="true" />
|
<ArrowLeft size={16} aria-hidden="true" />
|
||||||
Forms
|
Forms
|
||||||
</Button>
|
</Button>
|
||||||
{definition && <strong>{localized.title}</strong>}
|
{definition && <strong>{localized.title}</strong>}
|
||||||
{instance && <StatusBadge status={editableLifecycle ? "active" : "inactive"} label={stateLabel(instance.status)} />}
|
{instance && <StatusBadge status={editableLifecycle ? "active" : "inactive"} label={stateLabel(instance.status)} />}
|
||||||
</>}
|
</span>}
|
||||||
primaryActions={instance && recordsAvailable && !editableLifecycle ?
|
primaryActions={instance && recordsAvailable && !editableLifecycle ?
|
||||||
<Button onClick={() => navigate(recordFilingPath(instance))}>
|
<Button onClick={() => navigate(recordFilingPath(instance))}>
|
||||||
<Archive size={16} aria-hidden="true" />
|
<Archive size={16} aria-hidden="true" />
|
||||||
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 &&
|
||||||
@@ -429,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 &&
|
||||||
@@ -437,8 +456,10 @@ export default function FormInstancePage({ settings, auth }: PlatformRouteContex
|
|||||||
<div className="form-evidence-heading">
|
<div className="form-evidence-heading">
|
||||||
<h2>Assisted intake context</h2>
|
<h2>Assisted intake context</h2>
|
||||||
<StatusBadge
|
<StatusBadge
|
||||||
status={assistedConfirmationCurrent ? "active" : "inactive"}
|
status={assistedConfirmationCurrent || (!editableLifecycle && assistedConfirmations.length > 0) ? "active" : "inactive"}
|
||||||
label={assistedConfirmationCurrent ? "Read-back current" : "Read-back required"}
|
label={editableLifecycle
|
||||||
|
? assistedConfirmationCurrent ? "Read-back current" : "Read-back required"
|
||||||
|
: assistedConfirmations.length > 0 ? "Read-back recorded" : "No read-back evidence"}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<DescriptionList columns={3} collapseAt="workspace" density="compact">
|
<DescriptionList columns={3} collapseAt="workspace" density="compact">
|
||||||
@@ -534,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>
|
||||||
@@ -697,26 +717,70 @@ export default function FormInstancePage({ settings, auth }: PlatformRouteContex
|
|||||||
<CoreFormField label="Confirming party reference">
|
<CoreFormField label="Confirming party reference">
|
||||||
<input value={assistedConfirmedBy} onChange={(event) => setAssistedConfirmedBy(event.target.value)} disabled={saving} required />
|
<input value={assistedConfirmedBy} onChange={(event) => setAssistedConfirmedBy(event.target.value)} disabled={saving} required />
|
||||||
</CoreFormField>
|
</CoreFormField>
|
||||||
<CoreFormField label="Value source" help="Applied to each populated field in this first assisted-intake slice.">
|
|
||||||
<select value={assistedSource} onChange={(event) => setAssistedSource(event.target.value as typeof assistedSource)} disabled={saving}>
|
|
||||||
<option value="person_statement">Person statement</option>
|
|
||||||
<option value="representative_statement">Representative statement</option>
|
|
||||||
<option value="document">Document</option>
|
|
||||||
<option value="system">Existing system</option>
|
|
||||||
<option value="derived">Derived by operator</option>
|
|
||||||
</select>
|
|
||||||
</CoreFormField>
|
|
||||||
<CoreFormField label="Source confidence">
|
|
||||||
<select value={assistedConfidence} onChange={(event) => setAssistedConfidence(event.target.value as typeof assistedConfidence)} disabled={saving}>
|
|
||||||
<option value="stated">Stated</option>
|
|
||||||
<option value="verified">Verified</option>
|
|
||||||
<option value="uncertain">Uncertain</option>
|
|
||||||
</select>
|
|
||||||
</CoreFormField>
|
|
||||||
<CoreFormField label={assistedOutcome === "confirmed" ? "Note (optional)" : "Correction or exception note"}>
|
<CoreFormField label={assistedOutcome === "confirmed" ? "Note (optional)" : "Correction or exception note"}>
|
||||||
<textarea rows={3} value={assistedNote} onChange={(event) => setAssistedNote(event.target.value)} disabled={saving} required={assistedOutcome !== "confirmed"} />
|
<textarea rows={3} value={assistedNote} onChange={(event) => setAssistedNote(event.target.value)} disabled={saving} required={assistedOutcome !== "confirmed"} />
|
||||||
</CoreFormField>
|
</CoreFormField>
|
||||||
</FormGrid>
|
</FormGrid>
|
||||||
|
<section className="form-assisted-field-sources" aria-labelledby="assisted-field-sources-heading">
|
||||||
|
<header>
|
||||||
|
<h3 id="assisted-field-sources-heading">Source of each populated value</h3>
|
||||||
|
<p>Classify every value independently. Use a governed party, document, or system reference instead of repeating sensitive evidence in free text.</p>
|
||||||
|
</header>
|
||||||
|
<div className="form-assisted-field-source-list">
|
||||||
|
{Object.entries(values)
|
||||||
|
.filter(([, value]) => value !== null && value !== undefined && value !== "")
|
||||||
|
.map(([fieldKey]) => {
|
||||||
|
const source = assistedFieldSources[fieldKey] ?? defaultAssistedFieldSource(
|
||||||
|
assistedConfirmedBy.trim() || assisted?.affected_party_ref || ""
|
||||||
|
);
|
||||||
|
const fieldLabel = localized.fieldLabels[fieldKey]
|
||||||
|
?? definition?.fields.find((field) => field.key === fieldKey)?.label
|
||||||
|
?? humanize(fieldKey);
|
||||||
|
return (
|
||||||
|
<fieldset key={fieldKey} className="form-assisted-field-source">
|
||||||
|
<legend>{fieldLabel}</legend>
|
||||||
|
<FormGrid columns={3} gap="small" collapseAt="workspace">
|
||||||
|
<CoreFormField label="Value source">
|
||||||
|
<select
|
||||||
|
value={source.source}
|
||||||
|
onChange={(event) => changeAssistedFieldSource(fieldKey, {
|
||||||
|
source: event.target.value as typeof source.source
|
||||||
|
})}
|
||||||
|
disabled={saving}>
|
||||||
|
<option value="person_statement">Person statement</option>
|
||||||
|
<option value="representative_statement">Representative statement</option>
|
||||||
|
<option value="document">Document</option>
|
||||||
|
<option value="system">Existing system</option>
|
||||||
|
<option value="derived">Derived by operator</option>
|
||||||
|
</select>
|
||||||
|
</CoreFormField>
|
||||||
|
<CoreFormField label="Source confidence">
|
||||||
|
<select
|
||||||
|
value={source.confidence}
|
||||||
|
onChange={(event) => changeAssistedFieldSource(fieldKey, {
|
||||||
|
confidence: event.target.value as typeof source.confidence
|
||||||
|
})}
|
||||||
|
disabled={saving}>
|
||||||
|
<option value="stated">Stated</option>
|
||||||
|
<option value="verified">Verified</option>
|
||||||
|
<option value="uncertain">Uncertain</option>
|
||||||
|
</select>
|
||||||
|
</CoreFormField>
|
||||||
|
<CoreFormField label="Declared by or source reference" help="Optional for derived values; otherwise identify the governed party, document, or source system.">
|
||||||
|
<input
|
||||||
|
value={source.declared_by_ref ?? ""}
|
||||||
|
onChange={(event) => changeAssistedFieldSource(fieldKey, {
|
||||||
|
declared_by_ref: event.target.value || null
|
||||||
|
})}
|
||||||
|
disabled={saving}
|
||||||
|
/>
|
||||||
|
</CoreFormField>
|
||||||
|
</FormGrid>
|
||||||
|
</fieldset>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
open={confirmingAcknowledgement}
|
open={confirmingAcknowledgement}
|
||||||
@@ -955,6 +1019,70 @@ function intakeContext(instance: FormInstance | null): AssistedIntakeContext | n
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function defaultAssistedFieldSource(
|
||||||
|
declaredByRef: string
|
||||||
|
): AssistedIntakeContext["field_sources"][string] {
|
||||||
|
return {
|
||||||
|
source: "person_statement",
|
||||||
|
confidence: "stated",
|
||||||
|
declared_by_ref: declaredByRef || null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function assistedSourcesForRevision(
|
||||||
|
instance: FormInstance,
|
||||||
|
assisted: AssistedIntakeContext | null,
|
||||||
|
confirmations: AssistedConfirmation[]
|
||||||
|
): AssistedIntakeContext["field_sources"] {
|
||||||
|
const currentConfirmation = confirmations.find(
|
||||||
|
(confirmation) => confirmation.instance_revision === instance.revision
|
||||||
|
);
|
||||||
|
const confirmedSources = currentConfirmation?.metadata?.field_sources;
|
||||||
|
return normalizeAssistedFieldSources(confirmedSources)
|
||||||
|
?? assisted?.field_sources
|
||||||
|
?? {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeAssistedFieldSources(
|
||||||
|
value: unknown
|
||||||
|
): AssistedIntakeContext["field_sources"] | null {
|
||||||
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
||||||
|
const result: AssistedIntakeContext["field_sources"] = {};
|
||||||
|
for (const [fieldKey, candidate] of Object.entries(value)) {
|
||||||
|
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) continue;
|
||||||
|
const source = (candidate as Record<string, unknown>).source;
|
||||||
|
const confidence = (candidate as Record<string, unknown>).confidence;
|
||||||
|
const declaredByRef = (candidate as Record<string, unknown>).declared_by_ref;
|
||||||
|
if (!isAssistedSource(source) || !isAssistedConfidence(confidence)) continue;
|
||||||
|
result[fieldKey] = {
|
||||||
|
source,
|
||||||
|
confidence,
|
||||||
|
declared_by_ref: typeof declaredByRef === "string" && declaredByRef.trim()
|
||||||
|
? declaredByRef
|
||||||
|
: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAssistedSource(
|
||||||
|
value: unknown
|
||||||
|
): value is AssistedIntakeContext["field_sources"][string]["source"] {
|
||||||
|
return [
|
||||||
|
"person_statement",
|
||||||
|
"representative_statement",
|
||||||
|
"document",
|
||||||
|
"system",
|
||||||
|
"derived"
|
||||||
|
].includes(String(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAssistedConfidence(
|
||||||
|
value: unknown
|
||||||
|
): value is AssistedIntakeContext["field_sources"][string]["confidence"] {
|
||||||
|
return ["stated", "verified", "uncertain"].includes(String(value));
|
||||||
|
}
|
||||||
|
|
||||||
function statusAccessMessage(mode: "authenticated" | "email_link" | "permanent_link"): string {
|
function statusAccessMessage(mode: "authenticated" | "email_link" | "permanent_link"): string {
|
||||||
if (mode === "authenticated") return "Only the bound applicant account can open this status page.";
|
if (mode === "authenticated") return "Only the bound applicant account can open this status page.";
|
||||||
if (mode === "email_link") return "The applicant uses this tracking ID and linked email address to request an expiring link.";
|
if (mode === "email_link") return "The applicant uses this tracking ID and linked email address to request an expiring link.";
|
||||||
@@ -989,9 +1117,11 @@ export function visibleGroups(definition: FormDefinition, values: Record<string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
function evaluateCondition(condition: NonNullable<FormFieldDefinition["visibility_condition"]>, values: Record<string, unknown>): boolean {
|
function evaluateCondition(condition: NonNullable<FormFieldDefinition["visibility_condition"]>, values: Record<string, unknown>): boolean {
|
||||||
if (condition.kind === "all") return condition.conditions.every((item) => evaluateCondition(item, values));
|
if ("conditions" in condition) {
|
||||||
if (condition.kind === "any") return condition.conditions.some((item) => evaluateCondition(item, values));
|
if (condition.kind === "all") return condition.conditions.every((item) => evaluateCondition(item, values));
|
||||||
if (condition.kind === "not") return !evaluateCondition(condition.conditions[0], values);
|
if (condition.kind === "any") return condition.conditions.some((item) => evaluateCondition(item, values));
|
||||||
|
return !evaluateCondition(condition.conditions[0], values);
|
||||||
|
}
|
||||||
const actual = values[condition.field_key];
|
const actual = values[condition.field_key];
|
||||||
const expected = condition.value;
|
const expected = condition.value;
|
||||||
switch (condition.operator) {
|
switch (condition.operator) {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { Button,
|
|||||||
type PlatformRouteContext
|
type PlatformRouteContext
|
||||||
} from "@govoplan/core-webui";
|
} from "@govoplan/core-webui";
|
||||||
import { listFormInstances, type FormInstance } from "../../api/formsRuntime";
|
import { listFormInstances, type FormInstance } from "../../api/formsRuntime";
|
||||||
import { FORMS_RUNTIME_DOCUMENTATION, FORMS_RUNTIME_I18N } from "./interfacePatterns";
|
import { FORMS_RUNTIME_DOCUMENTATION } from "./interfacePatterns";
|
||||||
import IntakeProfilesDialog from "./IntakeProfilesDialog";
|
import IntakeProfilesDialog from "./IntakeProfilesDialog";
|
||||||
import AssistedIntakeDialog from "./AssistedIntakeDialog";
|
import AssistedIntakeDialog from "./AssistedIntakeDialog";
|
||||||
import StatusAccessPoliciesDialog from "./StatusAccessPoliciesDialog";
|
import StatusAccessPoliciesDialog from "./StatusAccessPoliciesDialog";
|
||||||
@@ -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.",
|
||||||
@@ -120,10 +122,90 @@ const en = {
|
|||||||
"Submission received": "Submission received",
|
"Submission received": "Submission received",
|
||||||
"Receipt": "Receipt",
|
"Receipt": "Receipt",
|
||||||
"Submit Form": "Submit Form",
|
"Submit Form": "Submit Form",
|
||||||
"Submit this Form? The values and managed attachment references become an immutable submission revision.": "Submit this Form? The values and managed attachment references become an immutable submission revision."
|
"Submit this Form? The values and managed attachment references become an immutable submission revision.": "Submit this Form? The values and managed attachment references become an immutable submission revision.",
|
||||||
|
"Assisted intake": "Assisted intake",
|
||||||
|
"Start assisted intake": "Start assisted intake",
|
||||||
|
"Capture who is acting, for whom, through which channel, and for what purpose before entering Form values.": "Capture who is acting, for whom, through which channel, and for what purpose before entering Form values.",
|
||||||
|
"Cancel": "Cancel",
|
||||||
|
"Start session": "Start session",
|
||||||
|
"Loading assisted intake profiles": "Loading assisted intake profiles",
|
||||||
|
"No assisted intake profile is enabled. Ask a Forms Runtime administrator to add one for the published Form.": "No assisted intake profile is enabled. Ask a Forms Runtime administrator to add one for the published Form.",
|
||||||
|
"Intake channel": "Intake channel",
|
||||||
|
"Service counter": "Service counter",
|
||||||
|
"Telephone": "Telephone",
|
||||||
|
"Paper": "Paper",
|
||||||
|
"Email": "Email",
|
||||||
|
"Mobile service": "Mobile service",
|
||||||
|
"Representative": "Representative",
|
||||||
|
"Offline import": "Offline import",
|
||||||
|
"Affected party reference": "Affected party reference",
|
||||||
|
"Use the governed person or organization reference; do not enter a display name only.": "Use the governed person or organization reference; do not enter a display name only.",
|
||||||
|
"Represented party reference": "Represented party reference",
|
||||||
|
"Optional when the affected party is acting directly.": "Optional when the affected party is acting directly.",
|
||||||
|
"Authority basis": "Authority basis",
|
||||||
|
"Acting for self": "Acting for self",
|
||||||
|
"Documented representation": "Documented representation",
|
||||||
|
"Legal guardianship": "Legal guardianship",
|
||||||
|
"Statutory authority": "Statutory authority",
|
||||||
|
"Responsible function reference": "Responsible function reference",
|
||||||
|
"Purpose": "Purpose",
|
||||||
|
"Legal basis reference": "Legal basis reference",
|
||||||
|
"Consent basis": "Consent basis",
|
||||||
|
"Accessibility or communication support": "Accessibility or communication support",
|
||||||
|
"Separate multiple needs with commas.": "Separate multiple needs with commas.",
|
||||||
|
"Privacy and procedural notice was provided": "Privacy and procedural notice was provided",
|
||||||
|
"Record the fact of notice here; retain any separately required evidence through its owning module.": "Record the fact of notice here; retain any separately required evidence through its owning module.",
|
||||||
|
"Assisted intake context": "Assisted intake context",
|
||||||
|
"Read-back current": "Read-back current",
|
||||||
|
"Read-back required": "Read-back required",
|
||||||
|
"Read-back recorded": "Read-back recorded",
|
||||||
|
"No read-back evidence": "No read-back evidence",
|
||||||
|
"Channel": "Channel",
|
||||||
|
"Affected party": "Affected party",
|
||||||
|
"Represented party": "Represented party",
|
||||||
|
"Authority": "Authority",
|
||||||
|
"Responsible function": "Responsible function",
|
||||||
|
"Notice": "Notice",
|
||||||
|
"Provided": "Provided",
|
||||||
|
"Not recorded": "Not recorded",
|
||||||
|
"Language": "Language",
|
||||||
|
"Communication support": "Communication support",
|
||||||
|
"This context records provenance and does not bypass the published Form rules. Any saved correction invalidates the earlier read-back for submission.": "This context records provenance and does not bypass the published Form rules. Any saved correction invalidates the earlier read-back for submission.",
|
||||||
|
"Read back and submit": "Read back and submit",
|
||||||
|
"Record assisted read-back": "Record assisted read-back",
|
||||||
|
"Make the exact values and managed evidence available to the confirming party. Record corrections before continuing; the server binds this evidence to the current revision and submission payload.": "Make the exact values and managed evidence available to the confirming party. Record corrections before continuing; the server binds this evidence to the current revision and submission payload.",
|
||||||
|
"Record and continue": "Record and continue",
|
||||||
|
"Outcome": "Outcome",
|
||||||
|
"Confirmed without correction": "Confirmed without correction",
|
||||||
|
"Corrected and confirmed": "Corrected and confirmed",
|
||||||
|
"Confirmation unavailable": "Confirmation unavailable",
|
||||||
|
"Confirmation method": "Confirmation method",
|
||||||
|
"Spoken read-back": "Spoken read-back",
|
||||||
|
"Written preview": "Written preview",
|
||||||
|
"Accessible copy": "Accessible copy",
|
||||||
|
"Unavailable": "Unavailable",
|
||||||
|
"Confirming party reference": "Confirming party reference",
|
||||||
|
"Note (optional)": "Note (optional)",
|
||||||
|
"Correction or exception note": "Correction or exception note",
|
||||||
|
"Source of each populated value": "Source of each populated value",
|
||||||
|
"Classify every value independently. Use a governed party, document, or system reference instead of repeating sensitive evidence in free text.": "Classify every value independently. Use a governed party, document, or system reference instead of repeating sensitive evidence in free text.",
|
||||||
|
"Value source": "Value source",
|
||||||
|
"Person statement": "Person statement",
|
||||||
|
"Representative statement": "Representative statement",
|
||||||
|
"Document": "Document",
|
||||||
|
"Existing system": "Existing system",
|
||||||
|
"Derived by operator": "Derived by operator",
|
||||||
|
"Source confidence": "Source confidence",
|
||||||
|
"Stated": "Stated",
|
||||||
|
"Verified": "Verified",
|
||||||
|
"Uncertain": "Uncertain",
|
||||||
|
"Declared by or source reference": "Declared by or source reference",
|
||||||
|
"Optional for derived values; otherwise identify the governed party, document, or source system.": "Optional for derived values; otherwise identify the governed party, document, or source system."
|
||||||
} 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.",
|
||||||
@@ -243,7 +325,85 @@ const de: Record<keyof typeof en, string> = {
|
|||||||
"Submission received": "Übermittlung eingegangen",
|
"Submission received": "Übermittlung eingegangen",
|
||||||
"Receipt": "Beleg",
|
"Receipt": "Beleg",
|
||||||
"Submit Form": "Formular absenden",
|
"Submit Form": "Formular absenden",
|
||||||
"Submit this Form? The values and managed attachment references become an immutable submission revision.": "Dieses Formular absenden? Die Werte und Referenzen auf verwaltete Anhänge werden zu einer unveränderlichen Übermittlungsrevision."
|
"Submit this Form? The values and managed attachment references become an immutable submission revision.": "Dieses Formular absenden? Die Werte und Referenzen auf verwaltete Anhänge werden zu einer unveränderlichen Übermittlungsrevision.",
|
||||||
|
"Assisted intake": "Assistierte Erfassung",
|
||||||
|
"Start assisted intake": "Assistierte Erfassung starten",
|
||||||
|
"Capture who is acting, for whom, through which channel, and for what purpose before entering Form values.": "Erfassen Sie vor den Formularwerten, wer für wen, über welchen Kanal und zu welchem Zweck handelt.",
|
||||||
|
"Cancel": "Abbrechen",
|
||||||
|
"Start session": "Sitzung starten",
|
||||||
|
"Loading assisted intake profiles": "Profile für die assistierte Erfassung werden geladen",
|
||||||
|
"No assisted intake profile is enabled. Ask a Forms Runtime administrator to add one for the published Form.": "Es ist kein Profil für die assistierte Erfassung aktiviert. Bitten Sie die Formularadministration, eines für das veröffentlichte Formular einzurichten.",
|
||||||
|
"Intake channel": "Erfassungskanal",
|
||||||
|
"Service counter": "Serviceschalter",
|
||||||
|
"Telephone": "Telefon",
|
||||||
|
"Paper": "Papier",
|
||||||
|
"Email": "E-Mail",
|
||||||
|
"Mobile service": "Mobiler Dienst",
|
||||||
|
"Representative": "Vertretung",
|
||||||
|
"Offline import": "Offline-Import",
|
||||||
|
"Affected party reference": "Referenz der betroffenen Partei",
|
||||||
|
"Use the governed person or organization reference; do not enter a display name only.": "Verwenden Sie die geregelte Personen- oder Organisationsreferenz, nicht nur einen Anzeigenamen.",
|
||||||
|
"Represented party reference": "Referenz der vertretenen Partei",
|
||||||
|
"Optional when the affected party is acting directly.": "Optional, wenn die betroffene Partei selbst handelt.",
|
||||||
|
"Authority basis": "Befugnisgrundlage",
|
||||||
|
"Acting for self": "Handelt für sich selbst",
|
||||||
|
"Documented representation": "Dokumentierte Vertretung",
|
||||||
|
"Legal guardianship": "Gesetzliche Betreuung",
|
||||||
|
"Statutory authority": "Gesetzliche Befugnis",
|
||||||
|
"Responsible function reference": "Referenz der zuständigen Funktion",
|
||||||
|
"Purpose": "Zweck",
|
||||||
|
"Legal basis reference": "Referenz der Rechtsgrundlage",
|
||||||
|
"Consent basis": "Einwilligungsgrundlage",
|
||||||
|
"Accessibility or communication support": "Barrierefreiheits- oder Kommunikationsunterstützung",
|
||||||
|
"Separate multiple needs with commas.": "Trennen Sie mehrere Bedarfe durch Kommas.",
|
||||||
|
"Privacy and procedural notice was provided": "Datenschutz- und Verfahrenshinweis wurde erteilt",
|
||||||
|
"Record the fact of notice here; retain any separately required evidence through its owning module.": "Erfassen Sie hier die Erteilung; gesondert erforderliche Nachweise verbleiben im zuständigen Modul.",
|
||||||
|
"Assisted intake context": "Kontext der assistierten Erfassung",
|
||||||
|
"Read-back current": "Rücklesen aktuell",
|
||||||
|
"Read-back required": "Rücklesen erforderlich",
|
||||||
|
"Read-back recorded": "Rücklesen erfasst",
|
||||||
|
"No read-back evidence": "Kein Rücklesenachweis",
|
||||||
|
"Channel": "Kanal",
|
||||||
|
"Affected party": "Betroffene Partei",
|
||||||
|
"Represented party": "Vertretene Partei",
|
||||||
|
"Authority": "Befugnis",
|
||||||
|
"Responsible function": "Zuständige Funktion",
|
||||||
|
"Notice": "Hinweis",
|
||||||
|
"Provided": "Erteilt",
|
||||||
|
"Not recorded": "Nicht erfasst",
|
||||||
|
"Language": "Sprache",
|
||||||
|
"Communication support": "Kommunikationsunterstützung",
|
||||||
|
"This context records provenance and does not bypass the published Form rules. Any saved correction invalidates the earlier read-back for submission.": "Dieser Kontext erfasst die Herkunft und umgeht die Regeln des veröffentlichten Formulars nicht. Jede gespeicherte Korrektur macht das frühere Rücklesen für die Einreichung ungültig.",
|
||||||
|
"Read back and submit": "Rücklesen und absenden",
|
||||||
|
"Record assisted read-back": "Assistiertes Rücklesen erfassen",
|
||||||
|
"Make the exact values and managed evidence available to the confirming party. Record corrections before continuing; the server binds this evidence to the current revision and submission payload.": "Stellen Sie der bestätigenden Partei die exakten Werte und verwalteten Nachweise bereit. Erfassen Sie Korrekturen vor dem Fortfahren; der Server bindet diesen Nachweis an die aktuelle Revision und die Einreichungsnutzdaten.",
|
||||||
|
"Record and continue": "Erfassen und fortfahren",
|
||||||
|
"Outcome": "Ergebnis",
|
||||||
|
"Confirmed without correction": "Ohne Korrektur bestätigt",
|
||||||
|
"Corrected and confirmed": "Korrigiert und bestätigt",
|
||||||
|
"Confirmation unavailable": "Bestätigung nicht verfügbar",
|
||||||
|
"Confirmation method": "Bestätigungsmethode",
|
||||||
|
"Spoken read-back": "Mündliches Rücklesen",
|
||||||
|
"Written preview": "Schriftliche Vorschau",
|
||||||
|
"Accessible copy": "Barrierefreie Kopie",
|
||||||
|
"Unavailable": "Nicht verfügbar",
|
||||||
|
"Confirming party reference": "Referenz der bestätigenden Partei",
|
||||||
|
"Note (optional)": "Notiz (optional)",
|
||||||
|
"Correction or exception note": "Korrektur- oder Ausnahmenotiz",
|
||||||
|
"Source of each populated value": "Quelle jedes ausgefüllten Werts",
|
||||||
|
"Classify every value independently. Use a governed party, document, or system reference instead of repeating sensitive evidence in free text.": "Klassifizieren Sie jeden Wert einzeln. Verwenden Sie eine geregelte Partei-, Dokument- oder Systemreferenz, statt sensible Nachweise im Freitext zu wiederholen.",
|
||||||
|
"Value source": "Wertquelle",
|
||||||
|
"Person statement": "Angabe der Person",
|
||||||
|
"Representative statement": "Angabe der Vertretung",
|
||||||
|
"Document": "Dokument",
|
||||||
|
"Existing system": "Bestehendes System",
|
||||||
|
"Derived by operator": "Durch Bearbeitung abgeleitet",
|
||||||
|
"Source confidence": "Quellenvertrauen",
|
||||||
|
"Stated": "Angegeben",
|
||||||
|
"Verified": "Geprüft",
|
||||||
|
"Uncertain": "Unsicher",
|
||||||
|
"Declared by or source reference": "Erklärende Partei oder Quellenreferenz",
|
||||||
|
"Optional for derived values; otherwise identify the governed party, document, or source system.": "Bei abgeleiteten Werten optional; sonst die geregelte Partei, das Dokument oder Quellsystem angeben."
|
||||||
};
|
};
|
||||||
|
|
||||||
export const generatedTranslations: PlatformTranslations = { en, de };
|
export const generatedTranslations: PlatformTranslations = { en, de };
|
||||||
|
|||||||
+1
-1
@@ -15,7 +15,7 @@ const routeScopes = [
|
|||||||
export const formsRuntimeModule: PlatformWebModule = {
|
export const formsRuntimeModule: PlatformWebModule = {
|
||||||
id: "forms_runtime",
|
id: "forms_runtime",
|
||||||
label: "i18n:govoplan-forms-runtime.forms",
|
label: "i18n:govoplan-forms-runtime.forms",
|
||||||
version: "0.1.19",
|
version: "0.1.20",
|
||||||
dependencies: ["access", "forms"],
|
dependencies: ["access", "forms"],
|
||||||
optionalDependencies: ["files", "approvals", "workflow_engine", "portal", "cases", "records", "policy", "audit"],
|
optionalDependencies: ["files", "approvals", "workflow_engine", "portal", "cases", "records", "policy", "audit"],
|
||||||
translations: generatedTranslations,
|
translations: generatedTranslations,
|
||||||
|
|||||||
@@ -21,6 +21,21 @@
|
|||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-instance-toolbar-context {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-instance-toolbar-context > strong {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1 1 180px;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
.forms-runtime-list-viewport,
|
.forms-runtime-list-viewport,
|
||||||
.form-instance-viewport {
|
.form-instance-viewport {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -278,6 +293,46 @@
|
|||||||
font-size: 0.78rem;
|
font-size: 0.78rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-assisted-field-sources {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 18px;
|
||||||
|
padding-top: 16px;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-assisted-field-sources > header h3,
|
||||||
|
.form-assisted-field-sources > header p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-assisted-field-sources > header p {
|
||||||
|
margin-top: 5px;
|
||||||
|
color: var(--text-soft);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-assisted-field-source-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-assisted-field-source {
|
||||||
|
min-width: 0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-compact);
|
||||||
|
background: var(--surface-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-assisted-field-source legend {
|
||||||
|
padding-inline: 5px;
|
||||||
|
color: var(--text-strong);
|
||||||
|
font-size: 0.84rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
.form-status-policy-save {
|
.form-status-policy-save {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
@@ -557,6 +612,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
|
.form-instance-toolbar .page-action-slot-context {
|
||||||
|
max-width: 100%;
|
||||||
|
flex: 1 1 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-instance-toolbar-context {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-instance-toolbar-context .status-badge {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.form-instance-content {
|
.form-instance-content {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user