From 84ebf1dae22456885f53de4ab7c2a86a8ee03950 Mon Sep 17 00:00:00 2001 From: Oksamies Date: Wed, 19 Nov 2025 14:30:51 +0200 Subject: [PATCH] Enhance error handling across package Wiki routes with user-facing error mapping and improved loader functions --- .../cyberstorm-remix/app/p/tabs/Wiki/Wiki.tsx | 110 ++++---- .../app/p/tabs/Wiki/WikiContent.tsx | 3 +- .../app/p/tabs/Wiki/WikiFirstPage.tsx | 225 +++++++--------- .../app/p/tabs/Wiki/WikiNewPage.tsx | 83 ++++-- .../app/p/tabs/Wiki/WikiPage.tsx | 246 +++++++----------- .../app/p/tabs/Wiki/WikiPageEdit.tsx | 197 ++++++++------ 6 files changed, 435 insertions(+), 429 deletions(-) diff --git a/apps/cyberstorm-remix/app/p/tabs/Wiki/Wiki.tsx b/apps/cyberstorm-remix/app/p/tabs/Wiki/Wiki.tsx index 83907ac9f..98d22d37b 100644 --- a/apps/cyberstorm-remix/app/p/tabs/Wiki/Wiki.tsx +++ b/apps/cyberstorm-remix/app/p/tabs/Wiki/Wiki.tsx @@ -1,95 +1,100 @@ import { faPlus } from "@fortawesome/pro-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { - getPublicEnvVariables, - getSessionTools, -} from "cyberstorm/security/publicEnvVariables"; + NimbusAwaitErrorElement, + NimbusDefaultRouteErrorBoundary, +} from "cyberstorm/utils/errors/NimbusErrorBoundary"; +import { handleLoaderError } from "cyberstorm/utils/errors/handleLoaderError"; +import { createNotFoundMapping } from "cyberstorm/utils/errors/loaderMappings"; +import { throwUserFacingPayloadResponse } from "cyberstorm/utils/errors/userFacingErrorResponse"; +import { getLoaderTools } from "cyberstorm/utils/getLoaderTools"; import { Suspense } from "react"; import { Await, type LoaderFunctionArgs, Outlet, + useLoaderData, useOutletContext, } from "react-router"; -import { useLoaderData } from "react-router"; import { type OutletContextShape } from "~/root"; import { NewButton, NewIcon, SkeletonBox } from "@thunderstore/cyberstorm"; -import { DapperTs } from "@thunderstore/dapper-ts"; -import { getPackageWiki } from "@thunderstore/dapper-ts"; -import { ApiError } from "@thunderstore/thunderstore-api"; import "./Wiki.css"; +export const wikiErrorMappings = [ + createNotFoundMapping( + "Wiki not available.", + "We could not find the requested wiki." + ), +]; + export async function loader({ params }: LoaderFunctionArgs) { if (params.communityId && params.namespaceId && params.packageId) { - const publicEnvVariables = getPublicEnvVariables(["VITE_API_URL"]); - const dapper = new DapperTs(() => { + const { dapper } = getLoaderTools(); + try { + const wiki = await dapper.getPackageWiki( + params.namespaceId, + params.packageId + ); + return { - apiHost: publicEnvVariables.VITE_API_URL, - sessionId: undefined, + wiki, + communityId: params.communityId, + namespaceId: params.namespaceId, + packageId: params.packageId, + slug: params.slug, + permissions: undefined, }; - }); - - let wiki: Awaited> | undefined; - - try { - wiki = await dapper.getPackageWiki(params.namespaceId, params.packageId); } catch (error) { - if (error instanceof ApiError) { - if (error.response.status === 404) { - wiki = undefined; - } else { - wiki = undefined; - console.error("Error fetching package wiki:", error); - } - } + handleLoaderError(error, { mappings: wikiErrorMappings }); } - - return { - wiki: wiki, - communityId: params.communityId, - namespaceId: params.namespaceId, - packageId: params.packageId, - slug: params.slug, - permissions: undefined, - }; } else { - throw new Error("Namespace ID or Package ID is missing"); + throwUserFacingPayloadResponse({ + headline: "Wiki not available.", + description: "We could not find the requested wiki.", + category: "not_found", + status: 404, + }); } } export async function clientLoader({ params }: LoaderFunctionArgs) { if (params.communityId && params.namespaceId && params.packageId) { - const tools = getSessionTools(); - const dapper = new DapperTs(() => { - return { - apiHost: tools?.getConfig().apiHost, - sessionId: tools?.getConfig().sessionId, - }; - }); + const { dapper } = getLoaderTools(); - const wiki = dapper.getPackageWiki(params.namespaceId, params.packageId); + const wikiPromise = dapper.getPackageWiki( + params.namespaceId, + params.packageId + ); - const permissions = dapper.getPackagePermissions( + const permissionsPromise = dapper.getPackagePermissions( params.communityId, params.namespaceId, params.packageId ); return { - wiki: wiki, + wiki: wikiPromise, communityId: params.communityId, namespaceId: params.namespaceId, packageId: params.packageId, slug: params.slug, - permissions: permissions, + permissions: permissionsPromise, }; } else { - throw new Error("Namespace ID or Package ID is missing"); + throwUserFacingPayloadResponse({ + headline: "Wiki not available.", + description: "We could not find the requested wiki.", + category: "not_found", + status: 404, + }); } } +/** + * Displays the package wiki navigation and nested routes, relying on Suspense for data. + */ export default function Wiki() { const { wiki, communityId, namespaceId, packageId, slug, permissions } = useLoaderData(); @@ -100,7 +105,10 @@ export default function Wiki() {
- + } + > {(resolvedValue) => resolvedValue?.permissions.can_manage ? (
@@ -126,7 +134,7 @@ export default function Wiki() { } > - }> + }> {(resolvedValue) => resolvedValue && resolvedValue.pages.map((page, index) => { @@ -194,3 +202,7 @@ export default function Wiki() {
); } + +export function ErrorBoundary() { + return ; +} diff --git a/apps/cyberstorm-remix/app/p/tabs/Wiki/WikiContent.tsx b/apps/cyberstorm-remix/app/p/tabs/Wiki/WikiContent.tsx index 562ad7d01..9046e44fc 100644 --- a/apps/cyberstorm-remix/app/p/tabs/Wiki/WikiContent.tsx +++ b/apps/cyberstorm-remix/app/p/tabs/Wiki/WikiContent.tsx @@ -13,6 +13,7 @@ import { Heading, NewButton, NewIcon } from "@thunderstore/cyberstorm"; import { type PackageWikiPageResponseData } from "@thunderstore/thunderstore-api"; import "./Wiki.css"; +import { NimbusAwaitErrorElement } from "cyberstorm/utils/errors/NimbusErrorBoundary"; interface WikiContentProps { page: PackageWikiPageResponseData; @@ -51,7 +52,7 @@ export const WikiContent = memo(function WikiContent({
- + }> {(resolvedValue) => resolvedValue ? (
diff --git a/apps/cyberstorm-remix/app/p/tabs/Wiki/WikiFirstPage.tsx b/apps/cyberstorm-remix/app/p/tabs/Wiki/WikiFirstPage.tsx index 82533db20..0ff3dd663 100644 --- a/apps/cyberstorm-remix/app/p/tabs/Wiki/WikiFirstPage.tsx +++ b/apps/cyberstorm-remix/app/p/tabs/Wiki/WikiFirstPage.tsx @@ -1,185 +1,136 @@ import { - getPublicEnvVariables, - getSessionTools, -} from "cyberstorm/security/publicEnvVariables"; -import { Suspense, useMemo } from "react"; + NimbusAwaitErrorElement, + NimbusDefaultRouteErrorBoundary, +} from "cyberstorm/utils/errors/NimbusErrorBoundary"; +import { handleLoaderError } from "cyberstorm/utils/errors/handleLoaderError"; +import { throwUserFacingPayloadResponse } from "cyberstorm/utils/errors/userFacingErrorResponse"; +import { getLoaderTools } from "cyberstorm/utils/getLoaderTools"; +import { Suspense } from "react"; import { Await, type LoaderFunctionArgs } from "react-router"; import { useLoaderData } from "react-router"; -import { SkeletonBox } from "@thunderstore/cyberstorm"; -import { DapperTs } from "@thunderstore/dapper-ts"; -import { - getPackagePermissions, - getPackageWiki, - getPackageWikiPage, -} from "@thunderstore/dapper-ts"; -import { isApiError } from "@thunderstore/thunderstore-api"; +import { NewAlert, SkeletonBox } from "@thunderstore/cyberstorm"; import "./Wiki.css"; import { WikiContent } from "./WikiContent"; -type ResultType = { - wiki: Awaited> | undefined; - firstPage: ReturnType | undefined; - communityId: string; - namespaceId: string; - packageId: string; - permissions: ReturnType | undefined; -}; - export async function loader({ params }: LoaderFunctionArgs) { if (params.communityId && params.namespaceId && params.packageId) { - const publicEnvVariables = getPublicEnvVariables(["VITE_API_URL"]); - const dapper = new DapperTs(() => { - return { - apiHost: publicEnvVariables.VITE_API_URL, - sessionId: undefined, - }; - }); - let result: ResultType = { - wiki: undefined, - firstPage: undefined, - communityId: params.communityId, - namespaceId: params.namespaceId, - packageId: params.packageId, - permissions: undefined, - }; + const { dapper } = getLoaderTools(); try { const wiki = await dapper.getPackageWiki( params.namespaceId, params.packageId ); - const firstPage = dapper.getPackageWikiPage(wiki.pages[0].id); - result = { - wiki: wiki, - firstPage: firstPage, + const firstPage = + wiki.pages.length > 0 + ? await dapper.getPackageWikiPage(wiki.pages[0].id) + : undefined; + return { communityId: params.communityId, namespaceId: params.namespaceId, packageId: params.packageId, - permissions: undefined, + promises: Promise.all([wiki, firstPage, undefined]), }; } catch (error) { - if (isApiError(error)) { - // There is no wiki or the User does not have permission to view the wiki, return empty wiki and undefined firstPage - if (error.response.status === 404) { - result = { - wiki: undefined, - firstPage: undefined, - communityId: params.communityId, - namespaceId: params.namespaceId, - packageId: params.packageId, - permissions: undefined, - }; - } else { - throw error; - } - } else { - throw error; - } + handleLoaderError(error); } - return result; } else { - throw new Error("Namespace ID or Package ID is missing"); + throwUserFacingPayloadResponse({ + headline: "Wiki page not available.", + description: "We could not find the requested wiki page.", + category: "not_found", + status: 404, + }); } } export async function clientLoader({ params }: LoaderFunctionArgs) { if (params.communityId && params.namespaceId && params.packageId) { - const tools = getSessionTools(); - const dapper = new DapperTs(() => { - return { - apiHost: tools?.getConfig().apiHost, - sessionId: tools?.getConfig().sessionId, - }; - }); + const { dapper } = getLoaderTools(); - const permissions = dapper.getPackagePermissions( + const permissionsPromise = dapper.getPackagePermissions( params.communityId, params.namespaceId, params.packageId ); - let result: ResultType = { - wiki: undefined, - firstPage: undefined, + const wikiPromise = dapper.getPackageWiki( + params.namespaceId, + params.packageId + ); + + const firstPagePromise = wikiPromise.then((resolvedWiki) => { + if (!resolvedWiki || resolvedWiki.pages.length === 0) { + return undefined; + } + return dapper.getPackageWikiPage(resolvedWiki.pages[0].id); + }); + + return { communityId: params.communityId, namespaceId: params.namespaceId, packageId: params.packageId, - permissions: permissions, + promises: Promise.all([ + wikiPromise, + firstPagePromise, + permissionsPromise, + ]), }; - - try { - const wiki = await dapper.getPackageWiki( - params.namespaceId, - params.packageId - ); - const firstPage = dapper.getPackageWikiPage(wiki.pages[0].id); - result = { - wiki: wiki, - firstPage: firstPage, - communityId: params.communityId, - namespaceId: params.namespaceId, - packageId: params.packageId, - permissions: permissions, - }; - } catch (error) { - if (isApiError(error)) { - // There is no wiki or the User does not have permission to view the wiki, return empty wiki and undefined firstPage - if (error.response.status === 404) { - result = { - wiki: undefined, - firstPage: undefined, - communityId: params.communityId, - namespaceId: params.namespaceId, - packageId: params.packageId, - permissions: permissions, - }; - } else { - throw error; - } - } else { - throw error; - } - } - return result; } else { - throw new Error("Namespace ID or Package ID is missing"); + throwUserFacingPayloadResponse({ + headline: "Wiki page not available.", + description: "We could not find the requested wiki page.", + category: "not_found", + status: 404, + }); } } +/** + * Renders the first wiki page, deferring data resolution to Suspense. + */ export default function WikiFirstPage() { - const { wiki, firstPage, communityId, namespaceId, packageId, permissions } = - useLoaderData(); + const { communityId, namespaceId, packageId, promises } = useLoaderData< + typeof loader | typeof clientLoader + >(); - const wikiAndFirstPageMemo = useMemo( - () => Promise.all([wiki, firstPage]), - [wiki, firstPage] - ); + return ( + }> + }> + {(resolvedData) => { + const [wiki, firstPage, permissions] = resolvedData; + if (wiki && firstPage) { + const nextPage = + wiki.pages.length > 1 ? wiki.pages[1].slug : undefined; + + return ( + + ); + } - }> - - {(resolvedValue) => { - const [wiki, firstPage] = resolvedValue; - if (wiki && firstPage) { return ( - 1 ? wiki.pages[1].slug : undefined} - canManage={permissions?.then((perms) => - typeof perms === "undefined" - ? false - : perms.permissions.can_manage - )} - /> + + There are no wiki pages available yet. + ); - } - return <>There are no wiki pages available.; - }} - - ; + }} + + + ); +} + +export function ErrorBoundary() { + return ; } diff --git a/apps/cyberstorm-remix/app/p/tabs/Wiki/WikiNewPage.tsx b/apps/cyberstorm-remix/app/p/tabs/Wiki/WikiNewPage.tsx index 063744821..facfbcc1e 100644 --- a/apps/cyberstorm-remix/app/p/tabs/Wiki/WikiNewPage.tsx +++ b/apps/cyberstorm-remix/app/p/tabs/Wiki/WikiNewPage.tsx @@ -1,11 +1,15 @@ import { useStrongForm } from "cyberstorm/utils/StrongForm/useStrongForm"; +import { NimbusErrorBoundaryFallback } from "cyberstorm/utils/errors/NimbusErrorBoundary"; +import { resolveRouteErrorPayload } from "cyberstorm/utils/errors/resolveRouteErrorPayload"; +import { throwUserFacingPayloadResponse } from "cyberstorm/utils/errors/userFacingErrorResponse"; import { useReducer, useState } from "react"; import { type LoaderFunctionArgs, + useLoaderData, useNavigate, useOutletContext, + useRouteError, } from "react-router"; -import { useLoaderData } from "react-router"; import { Markdown } from "~/commonComponents/Markdown/Markdown"; import { type OutletContextShape } from "~/root"; @@ -20,6 +24,7 @@ import { classnames } from "@thunderstore/cyberstorm"; import { type PackageWikiPageCreateRequestData, UserFacingError, + formatUserFacingError, postPackageWikiPageCreate, } from "@thunderstore/thunderstore-api"; @@ -33,7 +38,12 @@ export async function loader({ params }: LoaderFunctionArgs) { packageId: params.packageId, }; } else { - throw new Error("Namespace ID or Package ID is missing"); + throwUserFacingPayloadResponse({ + headline: "Can't find package for wiki page creation.", + description: "We could not find the requested package.", + category: "not_found", + status: 404, + }); } } @@ -45,7 +55,12 @@ export async function clientLoader({ params }: LoaderFunctionArgs) { packageId: params.packageId, }; } else { - throw new Error("Namespace ID or Package ID is missing"); + throwUserFacingPayloadResponse({ + headline: "Can't find package for wiki page creation.", + description: "We could not find the requested package.", + category: "not_found", + status: 404, + }); } } @@ -54,23 +69,34 @@ export default function Wiki() { typeof loader | typeof clientLoader >(); - const outletContext = useOutletContext() as OutletContextShape; + return ( + + ); +} - const toast = useToast(); +type WikiNewPageContentProps = { + communityId: string; + namespaceId: string; + packageId: string; +}; +/** + * Provides the interactive form for creating a new wiki page. + */ +function WikiNewPageContent({ + communityId, + namespaceId, + packageId, +}: WikiNewPageContentProps) { + const outletContext = useOutletContext() as OutletContextShape; + const toast = useToast(); const navigate = useNavigate(); - const [selectedTab, setSelectedTab] = useState<"write" | "preview">("write"); - async function moveToWikiPage(slug: string) { - toast.addToast({ - csVariant: "info", - children: `Moving to the created wiki page`, - duration: 4000, - }); - navigate(`/c/${communityId}/p/${namespaceId}/${packageId}/wiki/${slug}`); - } - function formFieldUpdateAction( state: PackageWikiPageCreateRequestData, action: { @@ -89,6 +115,15 @@ export default function Wiki() { markdown_content: "# New page", }); + async function moveToWikiPage(slug: string) { + toast.addToast({ + csVariant: "info", + children: `Moving to the created wiki page`, + duration: 4000, + }); + navigate(`/c/${communityId}/p/${namespaceId}/${packageId}/wiki/${slug}`); + } + type SubmitorOutput = Awaited>; async function submitor(data: typeof formInputs): Promise { @@ -128,7 +163,7 @@ export default function Wiki() { onSubmitError: (error) => { toast.addToast({ csVariant: "danger", - children: `Error occurred: ${error.message || "Unknown error"}`, + children: formatUserFacingError(error), duration: 8000, }); }, @@ -221,3 +256,19 @@ export default function Wiki() { ); } + +/** + * Maps loader errors to user-facing alerts for the new wiki page route. + */ +export function ErrorBoundary() { + const error = useRouteError(); + const payload = resolveRouteErrorPayload(error); + + return ( + + ); +} diff --git a/apps/cyberstorm-remix/app/p/tabs/Wiki/WikiPage.tsx b/apps/cyberstorm-remix/app/p/tabs/Wiki/WikiPage.tsx index 417a36c5e..fedbdaa99 100644 --- a/apps/cyberstorm-remix/app/p/tabs/Wiki/WikiPage.tsx +++ b/apps/cyberstorm-remix/app/p/tabs/Wiki/WikiPage.tsx @@ -1,32 +1,19 @@ import { - getPublicEnvVariables, - getSessionTools, -} from "cyberstorm/security/publicEnvVariables"; -import { Suspense, useMemo } from "react"; + NimbusAwaitErrorElement, + NimbusDefaultRouteErrorBoundary, +} from "cyberstorm/utils/errors/NimbusErrorBoundary"; +import { handleLoaderError } from "cyberstorm/utils/errors/handleLoaderError"; +import { throwUserFacingPayloadResponse } from "cyberstorm/utils/errors/userFacingErrorResponse"; +import { getLoaderTools } from "cyberstorm/utils/getLoaderTools"; +import { Suspense } from "react"; import { Await, type LoaderFunctionArgs } from "react-router"; import { useLoaderData } from "react-router"; -import { SkeletonBox } from "@thunderstore/cyberstorm"; -import { DapperTs } from "@thunderstore/dapper-ts"; -import { - getPackagePermissions, - getPackageWiki, - getPackageWikiPage, -} from "@thunderstore/dapper-ts"; -import { isApiError } from "@thunderstore/thunderstore-api"; +import { NewAlert, SkeletonBox } from "@thunderstore/cyberstorm"; import "./Wiki.css"; import { WikiContent } from "./WikiContent"; -type ResultType = { - wiki: ReturnType | undefined; - page: ReturnType | undefined; - communityId: string; - namespaceId: string; - packageId: string; - permissions: ReturnType | undefined; -}; - export async function loader({ params }: LoaderFunctionArgs) { if ( params.communityId && @@ -34,56 +21,38 @@ export async function loader({ params }: LoaderFunctionArgs) { params.packageId && params.slug ) { - const publicEnvVariables = getPublicEnvVariables(["VITE_API_URL"]); - const dapper = new DapperTs(() => { - return { - apiHost: publicEnvVariables.VITE_API_URL, - sessionId: undefined, - }; - }); - - let result: ResultType = { - wiki: undefined, - page: undefined, - communityId: params.communityId, - namespaceId: params.namespaceId, - packageId: params.packageId, - permissions: undefined, - }; + const { dapper } = getLoaderTools(); try { - const wiki = dapper.getPackageWiki(params.namespaceId, params.packageId); - const page = dapper.getPackageWikiPage(params.slug); - result = { - wiki: wiki, - page: page, + const permissionsPromise = dapper.getPackagePermissions( + params.communityId, + params.namespaceId, + params.packageId + ); + + const wikiPromise = dapper.getPackageWiki( + params.namespaceId, + params.packageId + ); + + const pagePromise = dapper.getPackageWikiPage(params.slug); + + return { communityId: params.communityId, namespaceId: params.namespaceId, packageId: params.packageId, - permissions: undefined, + promises: Promise.all([wikiPromise, pagePromise, permissionsPromise]), }; } catch (error) { - if (isApiError(error)) { - // There is no wiki or the User does not have permission to view the wiki, return empty wiki and undefined firstPage - if (error.response.status === 404) { - result = { - wiki: undefined, - page: undefined, - communityId: params.communityId, - namespaceId: params.namespaceId, - packageId: params.packageId, - permissions: undefined, - }; - } else { - throw error; - } - } else { - throw error; - } + handleLoaderError(error); } - return result; } else { - throw new Error("Namespace ID or Package ID is missing"); + throwUserFacingPayloadResponse({ + headline: "Wiki page not available.", + description: "We could not find the requested wiki page.", + category: "not_found", + status: 404, + }); } } @@ -94,115 +63,88 @@ export async function clientLoader({ params }: LoaderFunctionArgs) { params.packageId && params.slug ) { - const tools = getSessionTools(); - const dapper = new DapperTs(() => { - return { - apiHost: tools?.getConfig().apiHost, - sessionId: tools?.getConfig().sessionId, - }; - }); + const { dapper } = getLoaderTools(); - const permissions = dapper.getPackagePermissions( + const permissionsPromise = dapper.getPackagePermissions( params.communityId, params.namespaceId, params.packageId ); - let result: ResultType = { - wiki: undefined, - page: undefined, + const wikiPromise = dapper.getPackageWiki( + params.namespaceId, + params.packageId + ); + + const pagePromise = dapper.getPackageWikiPage(params.slug); + + return { communityId: params.communityId, namespaceId: params.namespaceId, packageId: params.packageId, - permissions: permissions, + promises: Promise.all([wikiPromise, pagePromise, permissionsPromise]), }; - - try { - const wiki = dapper.getPackageWiki(params.namespaceId, params.packageId); - const page = dapper.getPackageWikiPage(params.slug); - result = { - wiki: wiki, - page: page, - communityId: params.communityId, - namespaceId: params.namespaceId, - packageId: params.packageId, - permissions: permissions, - }; - } catch (error) { - if (isApiError(error)) { - // There is no wiki or the User does not have permission to view the wiki, return empty wiki and undefined firstPage - if (error.response.status === 404) { - result = { - wiki: undefined, - page: undefined, - communityId: params.communityId, - namespaceId: params.namespaceId, - packageId: params.packageId, - permissions: permissions, - }; - } else { - throw error; - } - } else { - throw error; - } - } - return result; } else { - throw new Error("Namespace ID or Package ID is missing"); + throwUserFacingPayloadResponse({ + headline: "Wiki page not available.", + description: "We could not find the requested wiki page.", + category: "not_found", + status: 404, + }); } } +/** + * Displays a specific wiki page and navigational context using Suspense. + */ export default function WikiPage() { - const { wiki, page, communityId, namespaceId, packageId } = useLoaderData< + const { communityId, namespaceId, packageId, promises } = useLoaderData< typeof loader | typeof clientLoader >(); - const wikiAndPageMemo = useMemo( - () => Promise.all([wiki, page]), - [wiki, page] - ); - - }> - Error occurred while loading wiki page
} - > - {(resolvedValue) => { - const [wiki, page] = resolvedValue; - if (wiki && page) { - const currentPageIndex = wiki.pages.findIndex( - (p) => p.id === page.id - ); - - let previousPage = undefined; - let nextPage = undefined; - - if (currentPageIndex === 0) { - previousPage = undefined; - } else { - previousPage = wiki.pages[currentPageIndex - 1]?.slug; + return ( + }> + }> + {(resolvedData) => { + const [resolvedWiki, resolvedPage, resolvedPermissions] = + resolvedData; + if (resolvedWiki && resolvedPage) { + const currentPageIndex = resolvedWiki.pages.findIndex( + (resolved) => resolved.id === resolvedPage.id + ); + + const previousPage = + currentPageIndex > 0 + ? resolvedWiki.pages[currentPageIndex - 1]?.slug + : undefined; + + const nextPage = + currentPageIndex < resolvedWiki.pages.length - 1 + ? resolvedWiki.pages[currentPageIndex + 1]?.slug + : undefined; + + return ( + + ); } - if (currentPageIndex === wiki.pages.length) { - nextPage = undefined; - } else { - nextPage = wiki.pages[currentPageIndex + 1]?.slug; - } + return Wiki page not found.; + }} + + + ); +} - return ( - - ); - } - return <>Wiki Page Not Found; - }} -
-
; +export function ErrorBoundary() { + return ; } diff --git a/apps/cyberstorm-remix/app/p/tabs/Wiki/WikiPageEdit.tsx b/apps/cyberstorm-remix/app/p/tabs/Wiki/WikiPageEdit.tsx index c30cd9bfc..eb405b708 100644 --- a/apps/cyberstorm-remix/app/p/tabs/Wiki/WikiPageEdit.tsx +++ b/apps/cyberstorm-remix/app/p/tabs/Wiki/WikiPageEdit.tsx @@ -1,10 +1,13 @@ -import { - getPublicEnvVariables, - getSessionTools, -} from "cyberstorm/security/publicEnvVariables"; import { useStrongForm } from "cyberstorm/utils/StrongForm/useStrongForm"; -import { useReducer, useState } from "react"; import { + NimbusAwaitErrorElement, + NimbusDefaultRouteErrorBoundary, +} from "cyberstorm/utils/errors/NimbusErrorBoundary"; +import { throwUserFacingPayloadResponse } from "cyberstorm/utils/errors/userFacingErrorResponse"; +import { getLoaderTools } from "cyberstorm/utils/getLoaderTools"; +import { Suspense, useReducer, useState } from "react"; +import { + Await, type LoaderFunctionArgs, useNavigate, useOutletContext, @@ -16,60 +19,34 @@ import { type OutletContextShape } from "~/root"; import { Heading, Modal, + NewAlert, NewButton, NewLink, NewTextInput, + SkeletonBox, Tabs, useToast, } from "@thunderstore/cyberstorm"; import { classnames } from "@thunderstore/cyberstorm"; -import { DapperTs } from "@thunderstore/dapper-ts"; import { type PackageWikiPageEditRequestData, type PackageWikiPageResponseData, type RequestConfig, UserFacingError, deletePackageWikiPage, + formatUserFacingError, postPackageWikiPageEdit, } from "@thunderstore/thunderstore-api"; import { ApiAction } from "@thunderstore/ts-api-react-actions"; -import "./Wiki.css"; +type MaybePromise = T | Promise; -export async function loader({ params }: LoaderFunctionArgs) { - if ( - params.communityId && - params.namespaceId && - params.packageId && - params.slug - ) { - const publicEnvVariables = getPublicEnvVariables(["VITE_API_URL"]); - const dapper = new DapperTs(() => { - return { - apiHost: publicEnvVariables.VITE_API_URL, - sessionId: undefined, - }; - }); - const wiki = await dapper.getPackageWiki( - params.namespaceId, - params.packageId - ); - const pageId = wiki.pages.find((p) => p.slug === params.slug)?.id; - if (!pageId) { - throw new Error("Page not found"); - } - const page = await dapper.getPackageWikiPage(pageId); - - return { - page: page, - communityId: params.communityId, - namespaceId: params.namespaceId, - packageId: params.packageId, - }; - } else { - throw new Error("Namespace ID or Package ID is missing"); - } -} +type ResultType = { + page: MaybePromise; + communityId: string; + namespaceId: string; + packageId: string; +}; export async function clientLoader({ params }: LoaderFunctionArgs) { if ( @@ -78,45 +55,103 @@ export async function clientLoader({ params }: LoaderFunctionArgs) { params.packageId && params.slug ) { - const tools = getSessionTools(); - const dapper = new DapperTs(() => { - return { - apiHost: tools?.getConfig().apiHost, - sessionId: tools?.getConfig().sessionId, - }; - }); - const wiki = await dapper.getPackageWiki( - params.namespaceId, - params.packageId - ); - const pageId = wiki.pages.find((p) => p.slug === params.slug)?.id; - if (!pageId) { - throw new Error("Page not found"); - } - const page = await dapper.getPackageWikiPage(pageId); + const { dapper } = getLoaderTools(); + const pagePromise = dapper + .getPackageWiki(params.namespaceId, params.packageId) + .then((wiki) => { + if (!wiki) { + throwUserFacingPayloadResponse( + { + headline: "Wiki not available.", + description: "We could not find the requested wiki.", + category: "not_found", + status: 404, + }, + { statusOverride: 404 } + ); + } + + const pageId = wiki.pages.find( + (candidate) => candidate.slug === params.slug + )?.id; + + if (!pageId) { + throwUserFacingPayloadResponse( + { + headline: "Wiki page not available.", + description: "We could not find the requested wiki page.", + category: "not_found", + status: 404, + }, + { statusOverride: 404 } + ); + } + + return dapper.getPackageWikiPage(pageId); + }); return { - page: page, + page: pagePromise, communityId: params.communityId, namespaceId: params.namespaceId, packageId: params.packageId, - }; + } satisfies ResultType; } else { - throw new Error("Namespace ID or Package ID is missing"); + throwUserFacingPayloadResponse({ + headline: "Wiki page not available for edit.", + description: "We could not find the requested wiki page for editing.", + category: "not_found", + status: 404, + }); } } +/** + * Renders the wiki page editor and defers data resolution to Suspense. + */ export default function WikiEdit() { - const { page, communityId, namespaceId, packageId } = useLoaderData< - typeof loader | typeof clientLoader - >(); + const { page, communityId, namespaceId, packageId } = + useLoaderData(); - const outletContext = useOutletContext() as OutletContextShape; + return ( + }> + }> + {(resolvedPage) => + resolvedPage ? ( + + ) : ( + Wiki page not found. + ) + } + + + ); +} - const toast = useToast(); +type WikiEditContentProps = { + page: PackageWikiPageResponseData; + communityId: string; + namespaceId: string; + packageId: string; +}; +/** + * Provides the interactive wiki page edit form once the page data is ready. + */ +function WikiEditContent({ + page, + communityId, + namespaceId, + packageId, +}: WikiEditContentProps) { + const outletContext = useOutletContext() as OutletContextShape; + const toast = useToast(); const navigate = useNavigate(); - const [selectedTab, setSelectedTab] = useState<"write" | "preview">("write"); async function moveToWikiPage() { @@ -198,7 +233,7 @@ export default function WikiEdit() { onSubmitError: (error) => { toast.addToast({ csVariant: "danger", - children: `Error occurred: ${error.message || "Unknown error"}`, + children: formatUserFacingError(error), duration: 8000, }); }, @@ -277,12 +312,6 @@ export default function WikiEdit() { ) : ( )} - {/*
-
-
*/}
+ + + +
+ ); +} + +export function ErrorBoundary() { + return ; +} + +/** + * Confirmation modal to delete a wiki page and refresh the listing. + */ function DeletePackageWikiPageForm(props: { communityId: string; namespaceId: string; @@ -340,7 +389,7 @@ function DeletePackageWikiPageForm(props: { onSubmitError: (error) => { toast.addToast({ csVariant: "danger", - children: `Error occurred: ${error.message || "Unknown error"}`, + children: formatUserFacingError(error), duration: 8000, }); },