fix(approvals): usertest-panel hardening (0.81.0)

Panel findings against the reworked approvals page, fixed in place:

- Never fabricate the request time: ApprovalRecord.createdAt is
  nullable now; a missing created_at omits the line instead of
  rendering DateTime.now() (which drifted on refresh). Guard:
  approvals_origin_test pins the omit-on-null invariant.
- Copyable errors on approve/reject/batch via showChainErrorSnack
  (the hard project rule) — batch surfaces the first real cause.
- Reject requires a reason: ChainInlineHelp strip + confirm disabled
  while empty, no more silent close-and-nothing-happens.
- Batch approve applies the same no-data confirmation as the single
  path, naming how many selected requests carry no show: data.
- Plainer language: glossary "Vorgang (Flow)", history label FRAGE
  (was PROMPT), no-data hint drops developer jargon.
- One-click copy of the run id; history payload pretty-prints like
  the card.

Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
flemming-it 2026-07-26 16:05:55 +02:00
parent 28f6fe1a9a
commit f7d7427d91
9 changed files with 411 additions and 122 deletions

View file

@ -1095,13 +1095,20 @@
"approvalsPillRejected": "abgelehnt",
"approvalsPillExpired": "abgelaufen",
"approvalsPayloadPreview": "ZU PRÜFENDE DATEN",
"approvalsNoPayload": "Keine Daten zum Prüfen angehängt. Der system.approval-Schritt des Flows bestimmt über sein \"show:\"-Feld, was angezeigt wird — setzen Sie es, um die Daten hinter dieser Entscheidung sichtbar zu machen.",
"approvalsNoPayload": "Zu dieser Freigabe wurden keine Daten hinterlegt. Sie entscheiden ohne Detailansicht — fragen Sie im Zweifel die Person, die den Vorgang eingerichtet hat. (Technisch: das Feld \"show:\" des Freigabe-Schritts legt fest, was hier erscheint.)",
"approvalsNoDataConfirmTitle": "Ohne Prüfdaten freigeben?",
"approvalsNoDataConfirmBody": "Dieser Flow hat bewusst keine Prüfdaten hinterlegt (kein \"show:\" am Freigabe-Schritt). Sie können trotzdem freigeben — entscheiden dann aber, ohne die Daten hinter dieser Entscheidung gesehen zu haben.",
"approvalsNoDataConfirmAction": "Trotzdem freigeben",
"approvalsRequestFallback": "Freigabe für diesen Schritt erforderlich",
"approvalsFlowStepMeta": "Flow: {flow} · Schritt: {step}",
"approvalsIntroHelp": "Hier warten pausierte Flows auf Ihre Entscheidung. Jede Karte zeigt, welcher Flow an welchem Schritt hält und welche Daten er Ihnen vorlegt — Freigeben setzt den Flow fort, Ablehnen stoppt ihn mit Ihrer Begründung.",
"approvalsIntroHelp": "Hier warten pausierte Vorgänge (Flows) auf Ihre Entscheidung. Jede Karte zeigt, welcher Vorgang an welchem Schritt hält und welche Daten er Ihnen vorlegt — Freigeben setzt ihn fort, Ablehnen stoppt ihn mit Ihrer Begründung.",
"approvalsRejectDialogHelp": "Die Ablehnung stoppt den Vorgang an diesem Schritt und wird mit Ihrer Begründung im Prüfprotokoll festgehalten.",
"approvalsRejectReasonHelper": "Pflichtfeld — wird im Prüfprotokoll (Audit-Log) festgehalten.",
"approvalsBatchNoDataTitle": "Ohne Prüfdaten freigeben?",
"approvalsBatchNoDataBody": "{count, plural, =1{Von {total} ausgewählten Freigaben hat 1 keine Prüfdaten. Sie geben sie ohne Detailansicht frei.} other{Von {total} ausgewählten Freigaben haben {count} keine Prüfdaten. Sie geben sie ohne Detailansicht frei.}}",
"approvalsCopyRun": "Lauf-Kennung kopieren",
"approvalsRunCopied": "Lauf-Kennung kopiert",
"approvalsUnknownReviewer": "(unbekannt)",
"approvalsOriginLabel": "HERKUNFT",
"approvalsOriginProject": "Projekt",
"approvalsOriginRequested": "Angefordert",
@ -1111,7 +1118,7 @@
"approvalsApproveButton": "Freigeben",
"approvalsRejectButton": "Ablehnen",
"approvalsRejectDialogTitle": "Freigabe ablehnen",
"approvalsRejectReasonLabel": "Begründung (im Audit-Log festgehalten)",
"approvalsRejectReasonLabel": "Begründung",
"approvalsApprovedToast": "freigegeben · {flow} {step}",
"@approvalsApprovedToast": {
"placeholders": {
@ -1155,7 +1162,7 @@
"approvalsDialogDecided": "entschieden",
"approvalsDialogCreated": "erstellt",
"approvalsDialogReason": "Begründung",
"approvalsDialogPrompt": "PROMPT",
"approvalsDialogPrompt": "FRAGE",
"approvalsExpiresIn": "{minutes}m",
"@approvalsExpiresIn": {
"placeholders": {

View file

@ -1113,13 +1113,20 @@
"approvalsPillRejected": "rejected",
"approvalsPillExpired": "expired",
"approvalsPayloadPreview": "DATA TO REVIEW",
"approvalsNoPayload": "No data was attached for review. The flow's approval step chooses what to show via its \"show:\" field — set it to surface the data behind this decision.",
"approvalsNoPayload": "No data was attached to this approval. You are deciding without a detail view — when in doubt, ask whoever set up the process. (Technical: the approval step's \"show:\" field controls what appears here.)",
"approvalsNoDataConfirmTitle": "Approve without review data?",
"approvalsNoDataConfirmBody": "This flow deliberately attached no review data (no \"show:\" on its approval step). You can still approve — but you would be deciding without seeing the data behind this decision.",
"approvalsNoDataConfirmAction": "Approve anyway",
"approvalsRequestFallback": "Approval required for this step",
"approvalsFlowStepMeta": "Flow: {flow} · Step: {step}",
"approvalsIntroHelp": "Paused flows waiting for your decision. Each card shows which flow is holding at which step and what data it puts in front of you — Approve resumes the flow, Reject stops it with your reason.",
"approvalsIntroHelp": "Paused processes (flows) waiting for your decision. Each card shows which process is holding at which step and what data it puts in front of you — Approve resumes it, Reject stops it with your reason.",
"approvalsRejectDialogHelp": "Rejecting stops the process at this step and is recorded with your reason in the audit trail.",
"approvalsRejectReasonHelper": "Required — recorded in the audit log.",
"approvalsBatchNoDataTitle": "Approve without review data?",
"approvalsBatchNoDataBody": "{count, plural, =1{1 of {total} selected approvals has no review data. You are approving it without a detail view.} other{{count} of {total} selected approvals have no review data. You are approving them without a detail view.}}",
"approvalsCopyRun": "Copy run id",
"approvalsRunCopied": "Run id copied",
"approvalsUnknownReviewer": "(unknown)",
"approvalsOriginLabel": "ORIGIN",
"approvalsOriginProject": "Project",
"approvalsOriginRequested": "Requested",
@ -1135,7 +1142,7 @@
"approvalsApproveButton": "Approve",
"approvalsRejectButton": "Reject",
"approvalsRejectDialogTitle": "Reject approval",
"approvalsRejectReasonLabel": "Reason (recorded in audit log)",
"approvalsRejectReasonLabel": "Reason",
"approvalsApprovedToast": "approved · {flow} {step}",
"@approvalsApprovedToast": {
"placeholders": {
@ -1179,7 +1186,7 @@
"approvalsDialogDecided": "decided",
"approvalsDialogCreated": "created",
"approvalsDialogReason": "reason",
"approvalsDialogPrompt": "PROMPT",
"approvalsDialogPrompt": "QUESTION",
"approvalsExpiresIn": "{minutes}m",
"@approvalsExpiresIn": {
"placeholders": {

View file

@ -3521,7 +3521,7 @@ abstract class AppLocalizations {
/// No description provided for @approvalsNoPayload.
///
/// In en, this message translates to:
/// **'No data was attached for review. The flow\'s approval step chooses what to show via its \"show:\" field — set it to surface the data behind this decision.'**
/// **'No data was attached to this approval. You are deciding without a detail view — when in doubt, ask whoever set up the process. (Technical: the approval step\'s \"show:\" field controls what appears here.)'**
String get approvalsNoPayload;
/// No description provided for @approvalsNoDataConfirmTitle.
@ -3557,9 +3557,51 @@ abstract class AppLocalizations {
/// No description provided for @approvalsIntroHelp.
///
/// In en, this message translates to:
/// **'Paused flows waiting for your decision. Each card shows which flow is holding at which step and what data it puts in front of you — Approve resumes the flow, Reject stops it with your reason.'**
/// **'Paused processes (flows) waiting for your decision. Each card shows which process is holding at which step and what data it puts in front of you — Approve resumes it, Reject stops it with your reason.'**
String get approvalsIntroHelp;
/// No description provided for @approvalsRejectDialogHelp.
///
/// In en, this message translates to:
/// **'Rejecting stops the process at this step and is recorded with your reason in the audit trail.'**
String get approvalsRejectDialogHelp;
/// No description provided for @approvalsRejectReasonHelper.
///
/// In en, this message translates to:
/// **'Required — recorded in the audit log.'**
String get approvalsRejectReasonHelper;
/// No description provided for @approvalsBatchNoDataTitle.
///
/// In en, this message translates to:
/// **'Approve without review data?'**
String get approvalsBatchNoDataTitle;
/// No description provided for @approvalsBatchNoDataBody.
///
/// In en, this message translates to:
/// **'{count, plural, =1{1 of {total} selected approvals has no review data. You are approving it without a detail view.} other{{count} of {total} selected approvals have no review data. You are approving them without a detail view.}}'**
String approvalsBatchNoDataBody(num count, Object total);
/// No description provided for @approvalsCopyRun.
///
/// In en, this message translates to:
/// **'Copy run id'**
String get approvalsCopyRun;
/// No description provided for @approvalsRunCopied.
///
/// In en, this message translates to:
/// **'Run id copied'**
String get approvalsRunCopied;
/// No description provided for @approvalsUnknownReviewer.
///
/// In en, this message translates to:
/// **'(unknown)'**
String get approvalsUnknownReviewer;
/// No description provided for @approvalsOriginLabel.
///
/// In en, this message translates to:
@ -3617,7 +3659,7 @@ abstract class AppLocalizations {
/// No description provided for @approvalsRejectReasonLabel.
///
/// In en, this message translates to:
/// **'Reason (recorded in audit log)'**
/// **'Reason'**
String get approvalsRejectReasonLabel;
/// No description provided for @approvalsApprovedToast.
@ -3677,7 +3719,7 @@ abstract class AppLocalizations {
/// No description provided for @approvalsDialogPrompt.
///
/// In en, this message translates to:
/// **'PROMPT'**
/// **'QUESTION'**
String get approvalsDialogPrompt;
/// No description provided for @approvalsExpiresIn.

View file

@ -2037,7 +2037,7 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get approvalsNoPayload =>
'Keine Daten zum Prüfen angehängt. Der system.approval-Schritt des Flows bestimmt über sein \"show:\"-Feld, was angezeigt wird — setzen Sie es, um die Daten hinter dieser Entscheidung sichtbar zu machen.';
'Zu dieser Freigabe wurden keine Daten hinterlegt. Sie entscheiden ohne Detailansicht — fragen Sie im Zweifel die Person, die den Vorgang eingerichtet hat. (Technisch: das Feld \"show:\" des Freigabe-Schritts legt fest, was hier erscheint.)';
@override
String get approvalsNoDataConfirmTitle => 'Ohne Prüfdaten freigeben?';
@ -2060,7 +2060,40 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get approvalsIntroHelp =>
'Hier warten pausierte Flows auf Ihre Entscheidung. Jede Karte zeigt, welcher Flow an welchem Schritt hält und welche Daten er Ihnen vorlegt — Freigeben setzt den Flow fort, Ablehnen stoppt ihn mit Ihrer Begründung.';
'Hier warten pausierte Vorgänge (Flows) auf Ihre Entscheidung. Jede Karte zeigt, welcher Vorgang an welchem Schritt hält und welche Daten er Ihnen vorlegt — Freigeben setzt ihn fort, Ablehnen stoppt ihn mit Ihrer Begründung.';
@override
String get approvalsRejectDialogHelp =>
'Die Ablehnung stoppt den Vorgang an diesem Schritt und wird mit Ihrer Begründung im Prüfprotokoll festgehalten.';
@override
String get approvalsRejectReasonHelper =>
'Pflichtfeld — wird im Prüfprotokoll (Audit-Log) festgehalten.';
@override
String get approvalsBatchNoDataTitle => 'Ohne Prüfdaten freigeben?';
@override
String approvalsBatchNoDataBody(num count, Object total) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other:
'Von $total ausgewählten Freigaben haben $count keine Prüfdaten. Sie geben sie ohne Detailansicht frei.',
one:
'Von $total ausgewählten Freigaben hat 1 keine Prüfdaten. Sie geben sie ohne Detailansicht frei.',
);
return '$_temp0';
}
@override
String get approvalsCopyRun => 'Lauf-Kennung kopieren';
@override
String get approvalsRunCopied => 'Lauf-Kennung kopiert';
@override
String get approvalsUnknownReviewer => '(unbekannt)';
@override
String get approvalsOriginLabel => 'HERKUNFT';
@ -2090,8 +2123,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get approvalsRejectDialogTitle => 'Freigabe ablehnen';
@override
String get approvalsRejectReasonLabel =>
'Begründung (im Audit-Log festgehalten)';
String get approvalsRejectReasonLabel => 'Begründung';
@override
String approvalsApprovedToast(String flow, String step) {
@ -2130,7 +2162,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get approvalsDialogReason => 'Begründung';
@override
String get approvalsDialogPrompt => 'PROMPT';
String get approvalsDialogPrompt => 'FRAGE';
@override
String approvalsExpiresIn(int minutes) {

View file

@ -2042,7 +2042,7 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get approvalsNoPayload =>
'No data was attached for review. The flow\'s approval step chooses what to show via its \"show:\" field — set it to surface the data behind this decision.';
'No data was attached to this approval. You are deciding without a detail view — when in doubt, ask whoever set up the process. (Technical: the approval step\'s \"show:\" field controls what appears here.)';
@override
String get approvalsNoDataConfirmTitle => 'Approve without review data?';
@ -2064,7 +2064,40 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get approvalsIntroHelp =>
'Paused flows waiting for your decision. Each card shows which flow is holding at which step and what data it puts in front of you — Approve resumes the flow, Reject stops it with your reason.';
'Paused processes (flows) waiting for your decision. Each card shows which process is holding at which step and what data it puts in front of you — Approve resumes it, Reject stops it with your reason.';
@override
String get approvalsRejectDialogHelp =>
'Rejecting stops the process at this step and is recorded with your reason in the audit trail.';
@override
String get approvalsRejectReasonHelper =>
'Required — recorded in the audit log.';
@override
String get approvalsBatchNoDataTitle => 'Approve without review data?';
@override
String approvalsBatchNoDataBody(num count, Object total) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other:
'$count of $total selected approvals have no review data. You are approving them without a detail view.',
one:
'1 of $total selected approvals has no review data. You are approving it without a detail view.',
);
return '$_temp0';
}
@override
String get approvalsCopyRun => 'Copy run id';
@override
String get approvalsRunCopied => 'Run id copied';
@override
String get approvalsUnknownReviewer => '(unknown)';
@override
String get approvalsOriginLabel => 'ORIGIN';
@ -2094,7 +2127,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get approvalsRejectDialogTitle => 'Reject approval';
@override
String get approvalsRejectReasonLabel => 'Reason (recorded in audit log)';
String get approvalsRejectReasonLabel => 'Reason';
@override
String approvalsApprovedToast(String flow, String step) {
@ -2133,7 +2166,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get approvalsDialogReason => 'reason';
@override
String get approvalsDialogPrompt => 'PROMPT';
String get approvalsDialogPrompt => 'QUESTION';
@override
String approvalsExpiresIn(int minutes) {