fix(docs): page help buttons open the right topic + wiring guard
The Federation and Runs help buttons opened the architecture doc:
federation.md/runs.md existed as assets but had no _DocEntry, so
showFaiDoc resolved the unknown slug to _kDocs.first. Register both
(onWelcome: false, so they don't clutter the newcomer grid but are
reachable), split the Welcome grid onto the curated subset, and make
the unknown-slug fallback assert in debug instead of silently opening
the wrong topic.
Guard (no-bugfix-without-a-guard): doc_help_wiring_test.dart scans
lib/ for every showFaiDoc('slug') call and asserts each has a
registered entry AND both assets/docs/<slug>[_de].md files. Exposes
kKnownDocSlugs for the test.
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
parent
588f437395
commit
5a3f00bb2c
7 changed files with 226 additions and 6 deletions
|
|
@ -50,6 +50,10 @@
|
||||||
"welcomeDocFlowsBlurb": "YAML-Grundlagen, Templating-Referenz, das Extract→Summarize-Beispiel.",
|
"welcomeDocFlowsBlurb": "YAML-Grundlagen, Templating-Referenz, das Extract→Summarize-Beispiel.",
|
||||||
"welcomeDocApprovalsTitle": "Freigaben",
|
"welcomeDocApprovalsTitle": "Freigaben",
|
||||||
"welcomeDocApprovalsBlurb": "Human-in-the-Loop-Checkpoints — wann nutzen, wie das Audit-Log sie protokolliert.",
|
"welcomeDocApprovalsBlurb": "Human-in-the-Loop-Checkpoints — wann nutzen, wie das Audit-Log sie protokolliert.",
|
||||||
|
"welcomeDocFederationTitle": "Föderation",
|
||||||
|
"welcomeDocFederationBlurb": "Weitere Hubs (Satelliten) anbinden, damit ihre Fähigkeiten hier nutzbar werden — was ein Satellit ist und wie die Anbindung abläuft.",
|
||||||
|
"welcomeDocRunsTitle": "Hintergrund-Läufe",
|
||||||
|
"welcomeDocRunsBlurb": "Flows, die im Hintergrund weiterlaufen — wozu, wie man die Funktion einschaltet und die Läufe überwacht.",
|
||||||
"helpTooltip": "Hilfe",
|
"helpTooltip": "Hilfe",
|
||||||
"errApprovalRejected": "Freigabe vom Reviewer abgelehnt.",
|
"errApprovalRejected": "Freigabe vom Reviewer abgelehnt.",
|
||||||
"errApprovalRejectedHint": "Der Reviewer hat den Step abgelehnt. Begründung steht im Audit-Log.",
|
"errApprovalRejectedHint": "Der Reviewer hat den Step abgelehnt. Begründung steht im Audit-Log.",
|
||||||
|
|
@ -104,6 +108,7 @@
|
||||||
"sidebarSettingsTooltip": "Einstellungen (⌘;)",
|
"sidebarSettingsTooltip": "Einstellungen (⌘;)",
|
||||||
"sidebarChannelTooltip": "Aktiver Kanal — klicken zum Wechseln.\nproduction = stabil · beta = Vorabversion · dev = laufend · local = Arbeitsumgebung",
|
"sidebarChannelTooltip": "Aktiver Kanal — klicken zum Wechseln.\nproduction = stabil · beta = Vorabversion · dev = laufend · local = Arbeitsumgebung",
|
||||||
"buttonCancel": "Abbrechen",
|
"buttonCancel": "Abbrechen",
|
||||||
|
"buttonLearnMore": "Mehr erfahren",
|
||||||
"buttonSave": "Speichern",
|
"buttonSave": "Speichern",
|
||||||
"buttonClose": "Schließen",
|
"buttonClose": "Schließen",
|
||||||
"buttonRetry": "Erneut versuchen",
|
"buttonRetry": "Erneut versuchen",
|
||||||
|
|
@ -1764,6 +1769,8 @@
|
||||||
"federationCapabilities": "{count} angebotene Fähigkeiten",
|
"federationCapabilities": "{count} angebotene Fähigkeiten",
|
||||||
"federationAddDialogTitle": "Registrierungs-Token ausstellen",
|
"federationAddDialogTitle": "Registrierungs-Token ausstellen",
|
||||||
"federationNameLabel": "Satelliten-Name",
|
"federationNameLabel": "Satelliten-Name",
|
||||||
|
"federationAddIntro": "Ein Satellit ist ein weiterer Hub, den dieser Hub anbindet — dessen Fähigkeiten werden dann hier in Flows nutzbar. Sie vergeben einen Namen und erhalten ein einmaliges Token, mit dem sich der Satellit bei diesem Hub anmeldet.",
|
||||||
|
"federationNameHelp": "Frei wählbarer Name, um diesen Satelliten in der Liste wiederzuerkennen (z. B. Standort oder Zweck). Rein zur Anzeige — ändert nichts an der Technik.",
|
||||||
"federationIssueButton": "Ausstellen",
|
"federationIssueButton": "Ausstellen",
|
||||||
"federationIssueFailed": "Token konnte nicht ausgestellt werden: {error}",
|
"federationIssueFailed": "Token konnte nicht ausgestellt werden: {error}",
|
||||||
"federationEnrollmentTitle": "{name} einrichten",
|
"federationEnrollmentTitle": "{name} einrichten",
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,10 @@
|
||||||
"welcomeDocFlowsBlurb": "YAML basics, templating reference, the extract→summarize example.",
|
"welcomeDocFlowsBlurb": "YAML basics, templating reference, the extract→summarize example.",
|
||||||
"welcomeDocApprovalsTitle": "Approvals",
|
"welcomeDocApprovalsTitle": "Approvals",
|
||||||
"welcomeDocApprovalsBlurb": "Human-in-the-loop checkpoints — when to use them, how the audit log records them.",
|
"welcomeDocApprovalsBlurb": "Human-in-the-loop checkpoints — when to use them, how the audit log records them.",
|
||||||
|
"welcomeDocFederationTitle": "Federation",
|
||||||
|
"welcomeDocFederationBlurb": "Connect other hubs (satellites) so their capabilities become usable here — what a satellite is and how enrolment works.",
|
||||||
|
"welcomeDocRunsTitle": "Background runs",
|
||||||
|
"welcomeDocRunsBlurb": "Flows that keep working in the background — what they are for, how to switch the feature on and monitor the runs.",
|
||||||
"helpTooltip": "Help",
|
"helpTooltip": "Help",
|
||||||
"errApprovalRejected": "Approval rejected by reviewer.",
|
"errApprovalRejected": "Approval rejected by reviewer.",
|
||||||
"errApprovalRejectedHint": "The reviewer marked this step as rejected. Check the audit log for the reviewer's reason.",
|
"errApprovalRejectedHint": "The reviewer marked this step as rejected. Check the audit log for the reviewer's reason.",
|
||||||
|
|
@ -112,6 +116,7 @@
|
||||||
"sidebarSettingsTooltip": "Settings (⌘;)",
|
"sidebarSettingsTooltip": "Settings (⌘;)",
|
||||||
"sidebarChannelTooltip": "Active channel — click to switch.\nproduction = stable · beta = pre-release · dev = rolling · local = workspace",
|
"sidebarChannelTooltip": "Active channel — click to switch.\nproduction = stable · beta = pre-release · dev = rolling · local = workspace",
|
||||||
"buttonCancel": "Cancel",
|
"buttonCancel": "Cancel",
|
||||||
|
"buttonLearnMore": "Learn more",
|
||||||
"buttonSave": "Save",
|
"buttonSave": "Save",
|
||||||
"buttonClose": "Close",
|
"buttonClose": "Close",
|
||||||
"buttonRetry": "Retry",
|
"buttonRetry": "Retry",
|
||||||
|
|
@ -1793,6 +1798,8 @@
|
||||||
},
|
},
|
||||||
"federationAddDialogTitle": "Issue an enrollment token",
|
"federationAddDialogTitle": "Issue an enrollment token",
|
||||||
"federationNameLabel": "Satellite name",
|
"federationNameLabel": "Satellite name",
|
||||||
|
"federationAddIntro": "A satellite is another hub this hub connects to — its capabilities then become usable here in flows. You give it a name and receive a one-time token the satellite uses to enrol with this hub.",
|
||||||
|
"federationNameHelp": "A free-form name to recognize this satellite in the list (e.g. its location or purpose). Display only — it changes nothing technical.",
|
||||||
"federationIssueButton": "Issue",
|
"federationIssueButton": "Issue",
|
||||||
"federationIssueFailed": "Could not issue token: {error}",
|
"federationIssueFailed": "Could not issue token: {error}",
|
||||||
"@federationIssueFailed": {
|
"@federationIssueFailed": {
|
||||||
|
|
|
||||||
|
|
@ -398,6 +398,30 @@ abstract class AppLocalizations {
|
||||||
/// **'Human-in-the-loop checkpoints — when to use them, how the audit log records them.'**
|
/// **'Human-in-the-loop checkpoints — when to use them, how the audit log records them.'**
|
||||||
String get welcomeDocApprovalsBlurb;
|
String get welcomeDocApprovalsBlurb;
|
||||||
|
|
||||||
|
/// No description provided for @welcomeDocFederationTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Federation'**
|
||||||
|
String get welcomeDocFederationTitle;
|
||||||
|
|
||||||
|
/// No description provided for @welcomeDocFederationBlurb.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Connect other hubs (satellites) so their capabilities become usable here — what a satellite is and how enrolment works.'**
|
||||||
|
String get welcomeDocFederationBlurb;
|
||||||
|
|
||||||
|
/// No description provided for @welcomeDocRunsTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Background runs'**
|
||||||
|
String get welcomeDocRunsTitle;
|
||||||
|
|
||||||
|
/// No description provided for @welcomeDocRunsBlurb.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Flows that keep working in the background — what they are for, how to switch the feature on and monitor the runs.'**
|
||||||
|
String get welcomeDocRunsBlurb;
|
||||||
|
|
||||||
/// No description provided for @helpTooltip.
|
/// No description provided for @helpTooltip.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
@ -680,6 +704,12 @@ abstract class AppLocalizations {
|
||||||
/// **'Cancel'**
|
/// **'Cancel'**
|
||||||
String get buttonCancel;
|
String get buttonCancel;
|
||||||
|
|
||||||
|
/// No description provided for @buttonLearnMore.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Learn more'**
|
||||||
|
String get buttonLearnMore;
|
||||||
|
|
||||||
/// No description provided for @buttonSave.
|
/// No description provided for @buttonSave.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
@ -5347,6 +5377,18 @@ abstract class AppLocalizations {
|
||||||
/// **'Satellite name'**
|
/// **'Satellite name'**
|
||||||
String get federationNameLabel;
|
String get federationNameLabel;
|
||||||
|
|
||||||
|
/// No description provided for @federationAddIntro.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'A satellite is another hub this hub connects to — its capabilities then become usable here in flows. You give it a name and receive a one-time token the satellite uses to enrol with this hub.'**
|
||||||
|
String get federationAddIntro;
|
||||||
|
|
||||||
|
/// No description provided for @federationNameHelp.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'A free-form name to recognize this satellite in the list (e.g. its location or purpose). Display only — it changes nothing technical.'**
|
||||||
|
String get federationNameHelp;
|
||||||
|
|
||||||
/// No description provided for @federationIssueButton.
|
/// No description provided for @federationIssueButton.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
|
||||||
|
|
@ -180,6 +180,20 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
String get welcomeDocApprovalsBlurb =>
|
String get welcomeDocApprovalsBlurb =>
|
||||||
'Human-in-the-Loop-Checkpoints — wann nutzen, wie das Audit-Log sie protokolliert.';
|
'Human-in-the-Loop-Checkpoints — wann nutzen, wie das Audit-Log sie protokolliert.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get welcomeDocFederationTitle => 'Föderation';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get welcomeDocFederationBlurb =>
|
||||||
|
'Weitere Hubs (Satelliten) anbinden, damit ihre Fähigkeiten hier nutzbar werden — was ein Satellit ist und wie die Anbindung abläuft.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get welcomeDocRunsTitle => 'Hintergrund-Läufe';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get welcomeDocRunsBlurb =>
|
||||||
|
'Flows, die im Hintergrund weiterlaufen — wozu, wie man die Funktion einschaltet und die Läufe überwacht.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get helpTooltip => 'Hilfe';
|
String get helpTooltip => 'Hilfe';
|
||||||
|
|
||||||
|
|
@ -338,6 +352,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get buttonCancel => 'Abbrechen';
|
String get buttonCancel => 'Abbrechen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get buttonLearnMore => 'Mehr erfahren';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get buttonSave => 'Speichern';
|
String get buttonSave => 'Speichern';
|
||||||
|
|
||||||
|
|
@ -3164,6 +3181,14 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get federationNameLabel => 'Satelliten-Name';
|
String get federationNameLabel => 'Satelliten-Name';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get federationAddIntro =>
|
||||||
|
'Ein Satellit ist ein weiterer Hub, den dieser Hub anbindet — dessen Fähigkeiten werden dann hier in Flows nutzbar. Sie vergeben einen Namen und erhalten ein einmaliges Token, mit dem sich der Satellit bei diesem Hub anmeldet.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get federationNameHelp =>
|
||||||
|
'Frei wählbarer Name, um diesen Satelliten in der Liste wiederzuerkennen (z. B. Standort oder Zweck). Rein zur Anzeige — ändert nichts an der Technik.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get federationIssueButton => 'Ausstellen';
|
String get federationIssueButton => 'Ausstellen';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -181,6 +181,20 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
String get welcomeDocApprovalsBlurb =>
|
String get welcomeDocApprovalsBlurb =>
|
||||||
'Human-in-the-loop checkpoints — when to use them, how the audit log records them.';
|
'Human-in-the-loop checkpoints — when to use them, how the audit log records them.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get welcomeDocFederationTitle => 'Federation';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get welcomeDocFederationBlurb =>
|
||||||
|
'Connect other hubs (satellites) so their capabilities become usable here — what a satellite is and how enrolment works.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get welcomeDocRunsTitle => 'Background runs';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get welcomeDocRunsBlurb =>
|
||||||
|
'Flows that keep working in the background — what they are for, how to switch the feature on and monitor the runs.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get helpTooltip => 'Help';
|
String get helpTooltip => 'Help';
|
||||||
|
|
||||||
|
|
@ -338,6 +352,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get buttonCancel => 'Cancel';
|
String get buttonCancel => 'Cancel';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get buttonLearnMore => 'Learn more';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get buttonSave => 'Save';
|
String get buttonSave => 'Save';
|
||||||
|
|
||||||
|
|
@ -3159,6 +3176,14 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get federationNameLabel => 'Satellite name';
|
String get federationNameLabel => 'Satellite name';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get federationAddIntro =>
|
||||||
|
'A satellite is another hub this hub connects to — its capabilities then become usable here in flows. You give it a name and receive a one-time token the satellite uses to enrol with this hub.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get federationNameHelp =>
|
||||||
|
'A free-form name to recognize this satellite in the list (e.g. its location or purpose). Display only — it changes nothing technical.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get federationIssueButton => 'Issue';
|
String get federationIssueButton => 'Issue';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1048,14 +1048,28 @@ class _DocEntry {
|
||||||
final IconData icon;
|
final IconData icon;
|
||||||
final String Function(AppLocalizations) title;
|
final String Function(AppLocalizations) title;
|
||||||
final String Function(AppLocalizations) blurb;
|
final String Function(AppLocalizations) blurb;
|
||||||
|
|
||||||
|
/// Whether this entry is shown as a card in the Welcome page's
|
||||||
|
/// doc grid. Every entry is reachable via [showFaiDoc] regardless;
|
||||||
|
/// `onWelcome: false` keeps advanced topics (federation, runs) out
|
||||||
|
/// of the newcomer grid while still wiring their page help buttons.
|
||||||
|
final bool onWelcome;
|
||||||
|
|
||||||
const _DocEntry({
|
const _DocEntry({
|
||||||
required this.slug,
|
required this.slug,
|
||||||
required this.icon,
|
required this.icon,
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.blurb,
|
required this.blurb,
|
||||||
|
this.onWelcome = true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Every doc topic Studio can open in-app. The slug maps to
|
||||||
|
/// `assets/docs/<slug>[_<locale>].md`. Adding a page help button
|
||||||
|
/// (`showFaiDoc(context, '<slug>')`) requires a matching entry here
|
||||||
|
/// AND the asset files — `doc_help_wiring_test.dart` enforces both,
|
||||||
|
/// so a help button can never silently fall back to the wrong topic
|
||||||
|
/// again (federation/runs used to resolve to architecture).
|
||||||
final List<_DocEntry> _kDocs = <_DocEntry>[
|
final List<_DocEntry> _kDocs = <_DocEntry>[
|
||||||
_DocEntry(
|
_DocEntry(
|
||||||
slug: 'architecture',
|
slug: 'architecture',
|
||||||
|
|
@ -1087,17 +1101,47 @@ final List<_DocEntry> _kDocs = <_DocEntry>[
|
||||||
title: (l) => l.welcomeDocApprovalsTitle,
|
title: (l) => l.welcomeDocApprovalsTitle,
|
||||||
blurb: (l) => l.welcomeDocApprovalsBlurb,
|
blurb: (l) => l.welcomeDocApprovalsBlurb,
|
||||||
),
|
),
|
||||||
|
_DocEntry(
|
||||||
|
slug: 'federation',
|
||||||
|
icon: Icons.hub_outlined,
|
||||||
|
title: (l) => l.welcomeDocFederationTitle,
|
||||||
|
blurb: (l) => l.welcomeDocFederationBlurb,
|
||||||
|
onWelcome: false,
|
||||||
|
),
|
||||||
|
_DocEntry(
|
||||||
|
slug: 'runs',
|
||||||
|
icon: Icons.rocket_launch_outlined,
|
||||||
|
title: (l) => l.welcomeDocRunsTitle,
|
||||||
|
blurb: (l) => l.welcomeDocRunsBlurb,
|
||||||
|
onWelcome: false,
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/// The doc cards shown on the Welcome page (curated newcomer set).
|
||||||
|
final List<_DocEntry> _kWelcomeDocs =
|
||||||
|
_kDocs.where((d) => d.onWelcome).toList();
|
||||||
|
|
||||||
|
/// Slugs Studio can resolve in-app — exposed for the wiring guard
|
||||||
|
/// test so it can assert every `showFaiDoc` call has a home.
|
||||||
|
final Set<String> kKnownDocSlugs = _kDocs.map((d) => d.slug).toSet();
|
||||||
|
|
||||||
/// Public entry-point for the doc-reader sheet. Pass a slug
|
/// Public entry-point for the doc-reader sheet. Pass a slug
|
||||||
/// ("approvals", "audit", "security", "architecture", "flows")
|
/// ("approvals", "audit", "security", "architecture", "flows")
|
||||||
/// and the bottom sheet opens with the localized markdown.
|
/// and the bottom sheet opens with the localized markdown.
|
||||||
/// Returns null when the slug isn't registered — caller can
|
/// Returns null when the slug isn't registered — caller can
|
||||||
/// log + show a SnackBar.
|
/// log + show a SnackBar.
|
||||||
Future<void>? showFaiDoc(BuildContext context, String slug) {
|
Future<void>? showFaiDoc(BuildContext context, String slug) {
|
||||||
|
// A slug with no entry is a wiring bug (doc_help_wiring_test.dart
|
||||||
|
// guards against it). The fallback keeps the UI alive in release,
|
||||||
|
// but we assert in debug so the mistake surfaces during
|
||||||
|
// development rather than silently opening the wrong topic.
|
||||||
final entry = _kDocs.firstWhere(
|
final entry = _kDocs.firstWhere(
|
||||||
(d) => d.slug == slug,
|
(d) => d.slug == slug,
|
||||||
orElse: () => _kDocs.first,
|
orElse: () {
|
||||||
|
assert(false, 'showFaiDoc: unknown doc slug "$slug" — register it '
|
||||||
|
'in _kDocs and add assets/docs/$slug[_de].md');
|
||||||
|
return _kDocs.first;
|
||||||
|
},
|
||||||
);
|
);
|
||||||
return _DocReaderSheet.show(context, entry);
|
return _DocReaderSheet.show(context, entry);
|
||||||
}
|
}
|
||||||
|
|
@ -1130,9 +1174,9 @@ class _DocsRow extends StatelessWidget {
|
||||||
if (!twoCols) {
|
if (!twoCols) {
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
for (var i = 0; i < _kDocs.length; i++) ...[
|
for (var i = 0; i < _kWelcomeDocs.length; i++) ...[
|
||||||
if (i > 0) const SizedBox(height: ChainSpace.md),
|
if (i > 0) const SizedBox(height: ChainSpace.md),
|
||||||
_DocCard(entry: _kDocs[i]),
|
_DocCard(entry: _kWelcomeDocs[i]),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
@ -1144,12 +1188,12 @@ class _DocsRow extends StatelessWidget {
|
||||||
// card spans the full width so it reads as intentional
|
// card spans the full width so it reads as intentional
|
||||||
// rather than a lonely half-box with dead space beside it.
|
// rather than a lonely half-box with dead space beside it.
|
||||||
final rows = <Widget>[];
|
final rows = <Widget>[];
|
||||||
for (var i = 0; i < _kDocs.length; i += 2) {
|
for (var i = 0; i < _kWelcomeDocs.length; i += 2) {
|
||||||
if (rows.isNotEmpty) {
|
if (rows.isNotEmpty) {
|
||||||
rows.add(const SizedBox(height: ChainSpace.md));
|
rows.add(const SizedBox(height: ChainSpace.md));
|
||||||
}
|
}
|
||||||
final left = _kDocs[i];
|
final left = _kWelcomeDocs[i];
|
||||||
final right = i + 1 < _kDocs.length ? _kDocs[i + 1] : null;
|
final right = i + 1 < _kWelcomeDocs.length ? _kWelcomeDocs[i + 1] : null;
|
||||||
if (right == null) {
|
if (right == null) {
|
||||||
rows.add(_DocCard(entry: left));
|
rows.add(_DocCard(entry: left));
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
70
test/doc_help_wiring_test.dart
Normal file
70
test/doc_help_wiring_test.dart
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
// Doc-help wiring guard — a page help button must open the RIGHT
|
||||||
|
// topic. Every `showFaiDoc(context, '<slug>')` call in lib/ needs:
|
||||||
|
//
|
||||||
|
// 1. a registered _DocEntry (exposed as kKnownDocSlugs), and
|
||||||
|
// 2. the backing assets assets/docs/<slug>.md + <slug>_de.md.
|
||||||
|
//
|
||||||
|
// Without this, a slug with no entry silently fell back to the
|
||||||
|
// first doc ('architecture'): the Federation and Runs help buttons
|
||||||
|
// opened the architecture sheet even though federation.md/runs.md
|
||||||
|
// existed — the author wrote the docs, set the icon, but never
|
||||||
|
// wired the catalog, and nothing caught it. This test catches that
|
||||||
|
// whole class (per the no-bugfix-without-a-guard rule).
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
import 'package:chain_studio/pages/welcome.dart' show kKnownDocSlugs;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
final callSlugs = _showFaiDocSlugsInLib();
|
||||||
|
|
||||||
|
test('lib/ actually calls showFaiDoc somewhere (sanity)', () {
|
||||||
|
expect(
|
||||||
|
callSlugs,
|
||||||
|
isNotEmpty,
|
||||||
|
reason: 'no showFaiDoc calls found — did the scan regex go stale?',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('every showFaiDoc slug is registered in the doc catalog', () {
|
||||||
|
final unregistered = callSlugs.difference(kKnownDocSlugs);
|
||||||
|
expect(
|
||||||
|
unregistered,
|
||||||
|
isEmpty,
|
||||||
|
reason:
|
||||||
|
'These slugs are opened by a help button but have no _DocEntry, '
|
||||||
|
'so they silently fall back to the wrong topic. Register them in '
|
||||||
|
'_kDocs (lib/pages/welcome.dart): $unregistered',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('every registered doc slug has both locale assets', () {
|
||||||
|
final missing = <String>[];
|
||||||
|
for (final slug in kKnownDocSlugs) {
|
||||||
|
for (final path in ['assets/docs/$slug.md', 'assets/docs/${slug}_de.md']) {
|
||||||
|
if (!File(path).existsSync()) missing.add(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expect(
|
||||||
|
missing,
|
||||||
|
isEmpty,
|
||||||
|
reason: 'Registered doc slugs missing their markdown assets: $missing',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scan lib/ for `showFaiDoc(context, 'slug')` and collect the slugs.
|
||||||
|
Set<String> _showFaiDocSlugsInLib() {
|
||||||
|
final re = RegExp(r'''showFaiDoc\(\s*context\s*,\s*['"]([a-z0-9_-]+)['"]''');
|
||||||
|
final slugs = <String>{};
|
||||||
|
final dir = Directory('lib');
|
||||||
|
for (final f in dir.listSync(recursive: true).whereType<File>()) {
|
||||||
|
if (!f.path.endsWith('.dart')) continue;
|
||||||
|
for (final m in re.allMatches(f.readAsStringSync())) {
|
||||||
|
slugs.add(m.group(1)!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return slugs;
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue