Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions app/[filename]/ClientFallbackPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,13 @@ export default function ClientFallbackPage({ filename, searchParams }: ClientFal

const sanitizedBasePath = getSanitizedBasePath();

const brokenReferences = ruleResult._brokenReferences
? {
detected: ruleResult._brokenReferences.detected,
paths: ruleResult._brokenReferences.paths,
}
: null;

setData({
type: "rule",
rule: ruleResult.data.rule,
Expand All @@ -161,6 +168,7 @@ export default function ClientFallbackPage({ filename, searchParams }: ClientFal
query: ruleResult.query,
variables: ruleResult.variables,
},
brokenReferences,
});
setLoading(false);
return;
Expand Down Expand Up @@ -243,6 +251,7 @@ export default function ClientFallbackPage({ filename, searchParams }: ClientFal
rule: data.rule,
ruleCategoriesMapping: data.ruleCategoriesMapping,
sanitizedBasePath: data.sanitizedBasePath,
brokenReferences: data.brokenReferences,
}}
/>
</Section>
Expand Down
9 changes: 8 additions & 1 deletion app/[filename]/ServerRulePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { tinaField } from "tinacms/dist/react";
import { TinaMarkdown } from "tinacms/dist/rich-text";
import AuthorsCard from "@/components/AuthorsCard";
import Breadcrumbs from "@/components/Breadcrumbs";
import BrokenReferenceBanner from "@/components/BrokenReferenceBanner";
import CategoriesCard from "@/components/CategoriesCard";
import Discussion from "@/components/Discussion";
import HelpCard from "@/components/HelpCard";
Expand All @@ -12,11 +13,13 @@ import RuleActionButtons from "@/components/RuleActionButtons";
import { YouTubeShorts } from "@/components/shared/Youtube";
import { getMarkdownComponentMapping } from "@/components/tina-markdown/markdown-component-mapping";
import { Card } from "@/components/ui/card";
import type { BrokenReferences } from "@/app/[filename]/page";

export interface ServerRulePageProps {
rule: any;
ruleCategoriesMapping: { title: string; uri: string }[];
sanitizedBasePath: string;
brokenReferences?: BrokenReferences | null;
}

export type ServerRulePagePropsWithTinaProps = {
Expand All @@ -28,7 +31,7 @@ export default function ServerRulePage({ serverRulePageProps, tinaProps }: Serve
const { data } = tinaProps;
const rule = data?.rule;

const { ruleCategoriesMapping, sanitizedBasePath } = serverRulePageProps;
const { ruleCategoriesMapping, sanitizedBasePath, brokenReferences } = serverRulePageProps;

const primaryCategory = ruleCategoriesMapping?.[0];
const breadcrumbCategories = primaryCategory ? [{ title: primaryCategory.title, link: `/${primaryCategory.uri}` }] : undefined;
Expand All @@ -37,6 +40,10 @@ export default function ServerRulePage({ serverRulePageProps, tinaProps }: Serve
<>
<Breadcrumbs categories={breadcrumbCategories} breadcrumbText="This rule" />

{brokenReferences?.detected && (
<BrokenReferenceBanner brokenPaths={brokenReferences.paths} ruleUri={rule?.uri || ""} />
)}

<div className="layout-two-columns">
<Card dropShadow className="layout-main-section p-6">
{rule?.isArchived && rule?.archivedreason && (
Expand Down
52 changes: 46 additions & 6 deletions app/[filename]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,17 +70,56 @@ const getCategoryData = async (filename: string) => {
}
};

export interface BrokenReferences {
detected: boolean;
paths: string[];
}

const getRuleData = async (filename: string) => {
try {
const tinaProps = await client.queries.ruleData({
const basicProps = await client.queries.ruleDataBasic({
relativePath: filename + "/rule.mdx",
});

return {
data: tinaProps.data,
query: tinaProps.query,
variables: tinaProps.variables,
};
try {
const fullProps = await client.queries.ruleData({
relativePath: filename + "/rule.mdx",
});

return {
data: fullProps.data,
query: fullProps.query,
variables: fullProps.variables,
brokenReferences: null as BrokenReferences | null,
};
} catch (relatedError) {
const errorMessage = relatedError instanceof Error ? relatedError.message : String(relatedError);

// Extract all broken paths from error message (there may be multiple)
const brokenPathMatches = errorMessage.matchAll(/Unable to find record ([^\n]+)/g);
const brokenPaths = Array.from(brokenPathMatches, (match) => match[1].trim());

// Fallback if no paths found
if (brokenPaths.length === 0) {
brokenPaths.push("unknown path");
}

return {
data: {
...basicProps.data,
rule: {
...basicProps.data.rule,
related: [], // Clear broken related rules
},
},
query: basicProps.query,
variables: basicProps.variables,
brokenReferences: {
detected: true,
paths: brokenPaths,
} as BrokenReferences,
};
}
} catch (error) {
console.error(`[getRuleData] failed for filename="${filename}":`, error);
return null;
Expand Down Expand Up @@ -272,6 +311,7 @@ export default async function Page({
rule: rule.data.rule,
ruleCategoriesMapping: ruleCategoriesMapping,
sanitizedBasePath: sanitizedBasePath,
brokenReferences: rule.brokenReferences,
}}
/>
</Section>
Expand Down
58 changes: 50 additions & 8 deletions app/api/tina/rule/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,58 @@ export async function GET(request: NextRequest) {
if (!relativePath) {
return NextResponse.json({ error: "relativePath is required" }, { status: 400 });
}

const fetchOptions = await getFetchOptions();
// Fetch rule using the Tina client with branch support
let result;
if (fetchOptions) {
result = await client.queries.ruleData({ relativePath }, fetchOptions);
} else {
result = await client.queries.ruleData({ relativePath });
}

return NextResponse.json(result, { status: 200 });
// First, try the basic query (without related field resolution)
// This ensures we always get the rule data even if references are broken
const basicResult = fetchOptions
? await client.queries.ruleDataBasic({ relativePath }, fetchOptions)
: await client.queries.ruleDataBasic({ relativePath });

// Now try the full query to get related rules
try {
const fullResult = fetchOptions
? await client.queries.ruleData({ relativePath }, fetchOptions)
: await client.queries.ruleData({ relativePath });

// Full query succeeded - no broken references
return NextResponse.json(fullResult, { status: 200 });
} catch (relatedError) {
// Full query failed but basic succeeded - broken references detected
const errorMessage = relatedError instanceof Error ? relatedError.message : String(relatedError);

// Extract all broken paths from error message (there may be multiple)
const brokenPathMatches = errorMessage.matchAll(/Unable to find record ([^\n]+)/g);
const brokenPaths = Array.from(brokenPathMatches, (match) => match[1].trim());

// Fallback if no paths found
if (brokenPaths.length === 0) {
brokenPaths.push("unknown path");
}

console.warn(`Broken related rule references detected: ${brokenPaths.join(", ")}`);

// Return basic result with metadata about the broken references
return NextResponse.json(
{
...basicResult,
data: {
...basicResult.data,
rule: {
...basicResult.data.rule,
related: [], // Clear broken related rules
},
},
_brokenReferences: {
detected: true,
paths: brokenPaths,
message: `This rule has broken related rule references. Please edit the rule to fix this.`,
},
},
{ status: 200 }
);
}
} catch (error) {
console.error("Error fetching rule from Tina:", error);
return NextResponse.json({ error: "Failed to fetch rule", details: error instanceof Error ? error.message : String(error) }, { status: 500 });
Expand Down
58 changes: 58 additions & 0 deletions components/BrokenReferenceBanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"use client";

import Link from "next/link";
import { useIsAdminPage } from "@/components/hooks/useIsAdminPage";

interface BrokenReferenceBannerProps {
brokenPaths: string[];
ruleUri: string;
}

export default function BrokenReferenceBanner({ brokenPaths, ruleUri }: BrokenReferenceBannerProps) {
const { isAdmin } = useIsAdminPage();

const adminUrl = `/admin#/~/rules/${encodeURIComponent(ruleUri)}`;

return (
<div className="mb-8 bg-amber-50 border border-amber-200 rounded-lg p-4">
<div className="flex items-start gap-3">
<div className="shrink-0">
<svg className="h-5 w-5 text-amber-600" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path
fillRule="evenodd"
d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z"
clipRule="evenodd"
/>
</svg>
</div>
<div className="flex-1">
<h3 className="text-sm font-semibold text-amber-800 m-0 mb-1">
Broken Related Rule Reference{brokenPaths.length > 1 ? "s" : ""}
</h3>
<p className="text-sm text-amber-700 m-0 mb-2">
This rule references {brokenPaths.length > 1 ? "rules that no longer exist" : "a rule that no longer exists"}:
</p>
<ul className="text-sm text-amber-600 list-disc list-inside mb-2 m-0 p-0">
{brokenPaths.map((path, idx) => (
<li key={idx} className="font-mono text-xs break-all">
{path}
</li>
))}
</ul>
{isAdmin ? (
<p className="text-sm text-amber-700 m-0">
Please edit the <strong>Related Rules</strong> field to remove or update the broken reference.
</p>
) : (
<p className="text-sm text-amber-700 m-0">
<Link href={adminUrl} className="text-amber-800 underline hover:text-amber-900">
Open in admin panel
</Link>{" "}
to fix this issue.
</p>
)}
</div>
</div>
</div>
);
}
49 changes: 49 additions & 0 deletions tina/queries/queries.gql
Original file line number Diff line number Diff line change
Expand Up @@ -290,3 +290,52 @@ query ruleData($relativePath: String!) {
}
}
}

# Fallback rule query that doesn't resolve related rule references
# Used when ruleData fails due to broken/missing related rule paths
query ruleDataBasic($relativePath: String!) {
rule(relativePath: $relativePath) {
... on Document {
_sys {
filename
basename
hasReferences
breadcrumbs
path
relativePath
extension
}
id
}
... on Rule {
title
uri
body
authors {
title
url
}
categories {
category {
... on CategoryCategory {
uri
title
}
}
}
# Note: related field omitted - this query is used when related rules are broken
redirects
guid
seoDescription
created
createdBy
createdByEmail
lastUpdated
lastUpdatedBy
lastUpdatedByEmail
isArchived
archivedreason
sidebarVideo
}
}
}