From d34702cffa03a365964b19a4bb22d4271e1afd6a Mon Sep 17 00:00:00 2001 From: Oksamies Date: Tue, 2 Dec 2025 14:36:05 +0200 Subject: [PATCH 1/4] (ts-api-react-actions) Enhance error handling in API actions and hooks with user-facing error mapping --- .../ts-api-react-actions/src/ApiAction.tsx | 23 +++++---- .../ts-api-react-actions/src/useApiAction.ts | 34 ++++++++++--- packages/ts-api-react/src/index.ts | 2 +- packages/ts-api-react/src/useApiCall.ts | 50 +++++++++++++++++-- 4 files changed, 88 insertions(+), 21 deletions(-) diff --git a/packages/ts-api-react-actions/src/ApiAction.tsx b/packages/ts-api-react-actions/src/ApiAction.tsx index 925b218a3..ccc385520 100644 --- a/packages/ts-api-react-actions/src/ApiAction.tsx +++ b/packages/ts-api-react-actions/src/ApiAction.tsx @@ -1,10 +1,11 @@ import { useCallback } from "react"; import { - type ApiEndpointProps, - ApiError, + ApiEndpointProps, + UserFacingError, + mapApiErrorToUserFacingError, } from "@thunderstore/thunderstore-api"; -import { type ApiEndpoint } from "@thunderstore/ts-api-react"; +import { ApiEndpoint, UseApiCallOptions } from "@thunderstore/ts-api-react"; import { useApiAction } from "./useApiAction"; @@ -15,18 +16,18 @@ export interface ApiActionProps< Return, > { endpoint: ApiEndpoint; + apiCallOptions?: UseApiCallOptions; onSubmitSuccess?: (result: Awaited) => void; - onSubmitError?: (error: Error | ApiError) => void; + onSubmitError?: (error: UserFacingError) => void; } -// As of this moment ApiActions sole purpose is to gracefully handle errors from API calls export function ApiAction< Params extends object, QueryParams extends object, Data extends object, Return, >(props: ApiActionProps) { - const { endpoint, onSubmitSuccess, onSubmitError } = props; + const { endpoint, onSubmitSuccess, onSubmitError, apiCallOptions } = props; const submitHandler = useApiAction< Params, QueryParams, @@ -34,6 +35,7 @@ export function ApiAction< ReturnType >({ endpoint: endpoint, + apiCallOptions, }); const onSubmit = useCallback( async (onSubmitProps: ApiEndpointProps) => { @@ -43,14 +45,17 @@ export function ApiAction< onSubmitSuccess(result); } } catch (e) { + const mappedError = + e instanceof UserFacingError ? e : mapApiErrorToUserFacingError(e); + if (onSubmitError) { - onSubmitError(e as Error | ApiError); + onSubmitError(mappedError); } else { - throw e; + throw mappedError; } } }, - [onSubmitSuccess, onSubmitError] + [onSubmitSuccess, onSubmitError, apiCallOptions, submitHandler] ); return onSubmit; diff --git a/packages/ts-api-react-actions/src/useApiAction.ts b/packages/ts-api-react-actions/src/useApiAction.ts index 909705f7b..8752752a4 100644 --- a/packages/ts-api-react-actions/src/useApiAction.ts +++ b/packages/ts-api-react-actions/src/useApiAction.ts @@ -1,20 +1,38 @@ +import { useCallback } from "react"; + import { type ApiEndpointProps } from "@thunderstore/thunderstore-api"; -import { type ApiEndpoint, useApiCall } from "@thunderstore/ts-api-react"; +import { + type ApiEndpoint, + UseApiCallOptions, + useApiCall, +} from "@thunderstore/ts-api-react"; export type UseApiActionArgs = { endpoint: ApiEndpoint; + apiCallOptions?: UseApiCallOptions; }; +/** + * Hook that adapts `useApiCall` to an async submit handler suitable for form actions. + */ export function useApiAction( args: UseApiActionArgs -) { - const apiCall = useApiCall(args.endpoint); +): ( + props: ApiEndpointProps +) => Promise> { + const apiCall = useApiCall( + args.endpoint, + args.apiCallOptions + ); - const submitHandler = async ( - props: ApiEndpointProps - ) => { - return await apiCall(props); - }; + const submitHandler = useCallback( + async ( + props: ApiEndpointProps + ): Promise> => { + return await apiCall(props); + }, + [apiCall, args] + ); return submitHandler; } diff --git a/packages/ts-api-react/src/index.ts b/packages/ts-api-react/src/index.ts index 1bc3c8b42..fc837a8dd 100644 --- a/packages/ts-api-react/src/index.ts +++ b/packages/ts-api-react/src/index.ts @@ -18,4 +18,4 @@ export { export type { ContextInterface } from "./SessionContext"; export { StorageManager as NamespacedStorageManager } from "./storage"; export { useApiCall } from "./useApiCall"; -export type { ApiEndpoint } from "./useApiCall"; +export type { ApiEndpoint, UseApiCallOptions } from "./useApiCall"; diff --git a/packages/ts-api-react/src/useApiCall.ts b/packages/ts-api-react/src/useApiCall.ts index b7e7191c8..27f0a51e4 100644 --- a/packages/ts-api-react/src/useApiCall.ts +++ b/packages/ts-api-react/src/useApiCall.ts @@ -1,13 +1,57 @@ -import { type ApiEndpointProps } from "@thunderstore/thunderstore-api"; +import { + type ApiEndpointProps, + MapUserFacingErrorOptions, + mapApiErrorToUserFacingError, +} from "@thunderstore/thunderstore-api"; +/** + * Describes an API endpoint invocation with typed params and data payload. + */ export type ApiEndpoint = ( props: ApiEndpointProps ) => Return; +/** + * Options that control automatic error mapping for `useApiCall`. + */ +export type UseApiCallOptions = { + mapErrors?: boolean; + errorOptions?: MapUserFacingErrorOptions; +}; + +/** + * Wraps API endpoints to optionally map thrown errors into `UserFacingError` instances. + */ export function useApiCall( - endpoint: ApiEndpoint + endpoint: ApiEndpoint, + options: UseApiCallOptions = {} ): (props: ApiEndpointProps) => Return { + const shouldMapErrors = options.mapErrors ?? true; + return (props: ApiEndpointProps) => { - return endpoint(props); + try { + const result = endpoint(props); + + if (shouldMapErrors && isPromise(result)) { + return result.catch((error) => { + throw mapApiErrorToUserFacingError(error, options.errorOptions); + }) as Return; + } + + return result; + } catch (error) { + if (!shouldMapErrors) { + throw error; + } + throw mapApiErrorToUserFacingError(error, options.errorOptions); + } }; } + +function isPromise(value: unknown): value is Promise { + return ( + typeof value === "object" && + value !== null && + typeof (value as Promise).then === "function" + ); +} From 4a25139edb0fb8ac42847b7a14854a9bc857212c Mon Sep 17 00:00:00 2001 From: Oksamies Date: Tue, 2 Dec 2025 14:36:54 +0200 Subject: [PATCH 2/4] Refactor loader functions to enhance error handling and move to different file --- .../cyberstorm/utils/dapperClientLoaders.ts | 53 +-------------- .../cyberstorm/utils/getLoaderTools.ts | 68 +++++++++++++++++++ 2 files changed, 69 insertions(+), 52 deletions(-) create mode 100644 apps/cyberstorm-remix/cyberstorm/utils/getLoaderTools.ts diff --git a/apps/cyberstorm-remix/cyberstorm/utils/dapperClientLoaders.ts b/apps/cyberstorm-remix/cyberstorm/utils/dapperClientLoaders.ts index 180437e61..bed621adb 100644 --- a/apps/cyberstorm-remix/cyberstorm/utils/dapperClientLoaders.ts +++ b/apps/cyberstorm-remix/cyberstorm/utils/dapperClientLoaders.ts @@ -1,52 +1 @@ -import { getSessionTools } from "cyberstorm/security/publicEnvVariables"; -import { type LoaderFunctionArgs } from "react-router"; - -import { DapperTs } from "@thunderstore/dapper-ts"; -import { ApiError, type GenericApiError } from "@thunderstore/thunderstore-api"; - -/** - * TODO - * 1) This approach no longer handles different ApiErrors properly - * when the data isn't awaited in the clientLoader but returned as - * promises for the Suspense/Await elements to handle. Instead, any - * HTTP error codes are shown as 500 errors. This isn't fixed yet - * as it will be easier to do once upcoming project wide error - * handling changes are merged. - * 2) The purpose of this helper was to reduce boilerplate in different - * tab components of the team settings page. Half of that boilerplate - * is Dapper setup, the other is handling ApiErrors. As the latter is - * supposed to be handled elsewhere after the changes mentioned above, - * this helper might no longer have a valid reason to exist after the - * changes. - */ -export function makeTeamSettingsTabLoader( - dataFetcher: (dapper: DapperTs, teamName: string) => Promise -) { - return async function clientLoader({ params }: LoaderFunctionArgs) { - const teamName = params.namespaceId!; - - try { - const dapper = setupDapper(); - const data = await dataFetcher(dapper, teamName); - return { teamName, ...data }; - } catch (error) { - if (error instanceof ApiError) { - const status = error.response.status; - const statusText = - (error.responseJson as GenericApiError)?.detail ?? - error.response.statusText; - throw new Response(statusText, { status, statusText }); - } - throw error; - } - }; -} - -const setupDapper = () => { - const tools = getSessionTools(); - const config = tools?.getConfig(); - return new DapperTs(() => ({ - apiHost: config?.apiHost, - sessionId: config?.sessionId, - })); -}; +export { makeTeamSettingsTabLoader } from "cyberstorm/utils/getLoaderTools"; diff --git a/apps/cyberstorm-remix/cyberstorm/utils/getLoaderTools.ts b/apps/cyberstorm-remix/cyberstorm/utils/getLoaderTools.ts new file mode 100644 index 000000000..d72a3b5c9 --- /dev/null +++ b/apps/cyberstorm-remix/cyberstorm/utils/getLoaderTools.ts @@ -0,0 +1,68 @@ +import { + getPublicEnvVariables, + getSessionTools, +} from "cyberstorm/security/publicEnvVariables"; +import type { LoaderFunctionArgs } from "react-router"; + +import { DapperTs } from "@thunderstore/dapper-ts"; + +import { + ApiError, + type GenericApiError, +} from "../../../../packages/thunderstore-api/src"; +import { throwUserFacingPayloadResponse } from "./errors/userFacingErrorResponse"; + +export function getLoaderTools() { + let dapper: DapperTs; + let sessionTools: ReturnType | undefined; + if (import.meta.env.SSR) { + const publicEnvVariables = getPublicEnvVariables(["VITE_API_URL"]); + dapper = new DapperTs(() => { + return { + apiHost: publicEnvVariables.VITE_API_URL, + sessionId: undefined, + }; + }); + } else { + sessionTools = getSessionTools(); + const sessionConfig = sessionTools?.getConfig(); + dapper = new DapperTs(() => { + return { + apiHost: sessionConfig?.apiHost, + sessionId: sessionConfig?.sessionId, + }; + }); + } + return { dapper, sessionTools }; +} + +export function makeTeamSettingsTabLoader( + dataFetcher: (dapper: DapperTs, teamName: string) => Promise +) { + return async function clientLoader({ params }: LoaderFunctionArgs) { + const teamName = params.namespaceId; + if (!teamName) { + throwUserFacingPayloadResponse({ + headline: "Team not found.", + description: "We could not find the requested team.", + category: "not_found", + status: 404, + }); + } + + try { + const { dapper } = getLoaderTools(); + const data = await dataFetcher(dapper, teamName); + return { teamName, ...data }; + } catch (error) { + if (error instanceof ApiError) { + const status = error.response.status; + const statusText = + (error.responseJson as GenericApiError)?.detail ?? + error.response.statusText; + throw new Response(statusText, { status, statusText }); + } + throw error; + } + }; +} From 689944fcce67d2f310ef134d34628bb7d6a6f446 Mon Sep 17 00:00:00 2001 From: Oksamies Date: Tue, 2 Dec 2025 15:05:36 +0200 Subject: [PATCH 3/4] Enhance error handling in StrongForm and useFormToaster with user-facing error mapping --- .../utils/StrongForm/useStrongForm.ts | 322 ++++++++++++------ .../cyberstorm-forms/src/useFormToaster.ts | 36 +- 2 files changed, 255 insertions(+), 103 deletions(-) diff --git a/apps/cyberstorm-remix/cyberstorm/utils/StrongForm/useStrongForm.ts b/apps/cyberstorm-remix/cyberstorm/utils/StrongForm/useStrongForm.ts index 52b34259c..2dc06bff6 100644 --- a/apps/cyberstorm-remix/cyberstorm/utils/StrongForm/useStrongForm.ts +++ b/apps/cyberstorm-remix/cyberstorm/utils/StrongForm/useStrongForm.ts @@ -4,31 +4,117 @@ import { ParseError, RequestBodyParseError, RequestQueryParamsParseError, + UserFacingError, + mapApiErrorToUserFacingError, } from "@thunderstore/thunderstore-api"; -interface UseStrongFormProps< - Inputs, +/** + * Checks if two types are exactly identical. + * Returns `true` if A and B are strictly equal, `false` otherwise. + * This is useful for distinguishing between types that are assignable to each other + * (e.g. `string` and `string | number`) but not identical. + */ +type IsExact = (() => T extends A ? 1 : 2) extends () => T extends B + ? 1 + : 2 + ? (() => T extends B ? 1 : 2) extends () => T extends A ? 1 : 2 + ? true + : false + : false; + +/** + * Enforces the presence of a `refiner` prop when the submission data shape + * differs from the input shape. + * + * If `SubmissionDataShape` is identical to or a subtype of `Inputs`, the refiner + * is optional (defaults to identity/cast). + * Otherwise, a refiner is required to transform inputs into the submission shape. + */ +type RefinerRequirement = [ SubmissionDataShape, - RefinerError, - SubmissionOutput, +] extends [Inputs] + ? { + refiner?: (inputs: Inputs) => Promise; + } + : { + refiner: (inputs: Inputs) => Promise; + }; + +/** + * Enforces the presence of an `errorMapper` prop when a custom `SubmissionError` type is used. + * + * If `SubmissionError` is exactly `UserFacingError` (the default), the mapper is optional + * as `mapApiErrorToUserFacingError` is used by default. + * If a different error type is specified, a mapper must be provided to convert unknown errors + * into the expected `SubmissionError` type. + */ +type ErrorMapperRequirement = IsExact< SubmissionError, + UserFacingError +> extends true + ? { + errorMapper?: (error: unknown) => SubmissionError; + } + : { + errorMapper: (error: unknown) => SubmissionError; + }; + +interface UseStrongFormPropsBase< + Inputs, + SubmissionDataShape extends Inputs = Inputs, + RefinerError extends Error = Error, + SubmissionOutput = unknown, + SubmissionError = UserFacingError, > { inputs: Inputs; - refiner?: (inputs: Inputs) => Promise; - onRefineSuccess?: (output: SubmissionDataShape) => void; - onRefineError?: (error: RefinerError) => void; submitor: (data: SubmissionDataShape) => Promise; + onRefineSuccess?: (data: SubmissionDataShape) => void; + onRefineError?: (error: RefinerError) => void; onSubmitSuccess?: (output: SubmissionOutput) => void; onSubmitError?: (error: SubmissionError) => void; } -export function useStrongForm< +export type UseStrongFormProps< + Inputs, + SubmissionDataShape extends Inputs = Inputs, + RefinerError extends Error = Error, + SubmissionOutput = unknown, + SubmissionError = UserFacingError, +> = UseStrongFormPropsBase< Inputs, SubmissionDataShape, RefinerError, SubmissionOutput, - SubmissionError, - InputErrors, + SubmissionError +> & + RefinerRequirement & + ErrorMapperRequirement; + +export interface UseStrongFormReturn< + Inputs, + SubmissionDataShape extends Inputs = Inputs, + RefinerError extends Error = Error, + SubmissionOutput = unknown, + SubmissionError = UserFacingError, + InputErrors = Record, +> { + submit: () => Promise; + submitting: boolean; + submitOutput?: SubmissionOutput; + submitError?: SubmissionError; + submissionData?: SubmissionDataShape; + refining: boolean; + refineError?: RefinerError; + inputErrors?: InputErrors; +} + +export function useStrongForm< + Inputs, + SubmissionDataShape extends Inputs = Inputs, + RefinerError extends Error = Error, + SubmissionOutput = unknown, + SubmissionError = UserFacingError, + InputErrors = Record, >( props: UseStrongFormProps< Inputs, @@ -37,7 +123,14 @@ export function useStrongForm< SubmissionOutput, SubmissionError > -) { +): UseStrongFormReturn< + Inputs, + SubmissionDataShape, + RefinerError, + SubmissionOutput, + SubmissionError, + InputErrors +> { const [refining, setRefining] = useState(false); const [submissionData, setSubmissionData] = useState(); const [refineError, setRefineError] = useState(); @@ -46,112 +139,149 @@ export function useStrongForm< const [submitError, setSubmitError] = useState(); const [inputErrors, setInputErrors] = useState(); - useEffect(() => { - if (refining || submitting) { - return; + const ensureSubmissionDataShape = (value: Inputs): SubmissionDataShape => { + if ( + value === null || + (typeof value !== "object" && typeof value !== "function") + ) { + throw new Error( + "useStrongForm received primitive form inputs without a refiner; provide a refiner or ensure the input type matches the submission data shape." + ); } + + return value as SubmissionDataShape; + }; + + useEffect(() => { + let cancelled = false; + setSubmitOutput(undefined); setSubmitError(undefined); setInputErrors(undefined); - if (props.refiner) { - setSubmissionData(undefined); + + if (!props.refiner) { + setSubmissionData(ensureSubmissionDataShape(props.inputs)); + setRefining(false); setRefineError(undefined); - setRefining(true); - props - .refiner(props.inputs) - .then((refiningOutput) => { - if (props.onRefineSuccess) { - props.onRefineSuccess(refiningOutput); - } - setSubmissionData(refiningOutput); - setRefining(false); - }) - .catch((error) => { - setRefineError(error); - if (props.onRefineError) { - props.onRefineError(error); - } + return () => { + cancelled = true; + }; + } + + setSubmissionData(undefined); + setRefineError(undefined); + setRefining(true); + + props + .refiner(props.inputs) + .then((result) => { + if (cancelled) { + return; + } + + setSubmissionData(result); + if (props.onRefineSuccess) { + props.onRefineSuccess(result); + } + }) + .catch((error) => { + if (cancelled) { + return; + } + + const normalizedError = + error instanceof Error ? error : new Error(String(error)); + const castError = normalizedError as RefinerError; + setRefineError(castError); + if (props.onRefineError) { + props.onRefineError(castError); + } + }) + .finally(() => { + if (!cancelled) { setRefining(false); - }); - } else { - // A quick hack to allow the form to work without a refiner. - setSubmissionData(props.inputs as unknown as SubmissionDataShape); + } + }); + + return () => { + cancelled = true; + }; + }, [props.inputs, props.refiner, props.onRefineSuccess, props.onRefineError]); + + const toSubmissionError = (error: unknown): SubmissionError => { + if (props.errorMapper) { + return props.errorMapper(error); + } + + // If errorMapper is not provided, we assume SubmissionError is UserFacingError. + // This is enforced by the ErrorMapperRequirement type. + return mapApiErrorToUserFacingError(error) as unknown as SubmissionError; + }; + + const emitSubmissionError = (error: SubmissionError): never => { + setSubmitError(error); + if (props.onSubmitError) { + props.onSubmitError(error); } - }, [props.inputs]); + throw error; + }; + + const createGuardSubmissionError = (message: string): SubmissionError => { + return toSubmissionError( + new UserFacingError({ + category: "validation", + headline: message, + description: undefined, + originalError: new Error(message), + }) + ); + }; - const submit = async () => { + const submit = async (): Promise => { if (submitting) { - const error = new Error("Form is already submitting!"); - if (props.onSubmitError) { - props.onSubmitError(error as SubmissionError); - } - throw error; + return emitSubmissionError( + createGuardSubmissionError("Form is already submitting.") + ); } + if (refining) { - const error = new Error("Form is still refining!"); - if (props.onSubmitError) { - props.onSubmitError(error as SubmissionError); - } - throw error; + return emitSubmissionError( + createGuardSubmissionError("Form is still refining.") + ); } + if (refineError) { - const error = new Error("Form refinement failed!"); - if (props.onSubmitError) { - props.onSubmitError(error as SubmissionError); - } - throw refineError; + return emitSubmissionError(toSubmissionError(refineError)); } + if (!submissionData) { - const error = new Error("Form has not been refined yet!"); - if (props.onSubmitError) { - props.onSubmitError(error as SubmissionError); - } - throw error; + return emitSubmissionError( + createGuardSubmissionError("Form has not been refined yet.") + ); } setSubmitting(true); + setSubmitError(undefined); + setInputErrors(undefined); + try { - await props - .submitor(submissionData) - .then((output) => { - setSubmitOutput(output); - if (props.onSubmitSuccess) { - props.onSubmitSuccess(output); - } - }) - .catch((error) => { - if (error instanceof RequestBodyParseError) { - setSubmitError( - new Error( - "Some of the field values are invalid" - ) as SubmissionError - ); - setInputErrors(error.error.formErrors as InputErrors); - } else if (error instanceof RequestQueryParamsParseError) { - setSubmitError( - new Error( - "Some of the query parameters are invalid" - ) as SubmissionError - ); - setInputErrors(error.error.formErrors as InputErrors); - } else if (error instanceof ParseError) { - setSubmitError( - new Error( - "Request succeeded, but the response was invalid" - ) as SubmissionError - ); - setInputErrors(error.error.formErrors as InputErrors); - throw error; - } else { - throw error; - } - }); - return submitOutput; + const output = await props.submitor(submissionData); + setSubmitOutput(output); + if (props.onSubmitSuccess) { + props.onSubmitSuccess(output); + } + return output; } catch (error) { - if (props.onSubmitError) { - props.onSubmitError(error as SubmissionError); + if (error instanceof RequestBodyParseError) { + setInputErrors(error.error.formErrors.fieldErrors as InputErrors); + } else if (error instanceof RequestQueryParamsParseError) { + setInputErrors(error.error.formErrors.fieldErrors as InputErrors); + } else if (error instanceof ParseError) { + setInputErrors(error.error.formErrors.fieldErrors as InputErrors); } - throw error; + + const mappedError = toSubmissionError(error); + return emitSubmissionError(mappedError); } finally { setSubmitting(false); } diff --git a/packages/cyberstorm-forms/src/useFormToaster.ts b/packages/cyberstorm-forms/src/useFormToaster.ts index bbbc1cfad..1abcdeb3b 100644 --- a/packages/cyberstorm-forms/src/useFormToaster.ts +++ b/packages/cyberstorm-forms/src/useFormToaster.ts @@ -1,4 +1,8 @@ import { useToast } from "@thunderstore/cyberstorm"; +import { + UserFacingError, + formatUserFacingError, +} from "@thunderstore/thunderstore-api"; export type UseFormToasterArgs = { @@ -38,17 +42,35 @@ export function useFormToaster({ }); }, onSubmitError: (props) => { + const resolvedMessage = resolveErrorMessage(props, errorMessage); + toast.addToast({ csVariant: "danger", - children: errorMessage - ? typeof errorMessage === "string" - ? errorMessage - : props - ? errorMessage(props) - : "Unknown error occurred. The error has been logged" - : "Unknown error occurred. The error has been logged", + children: resolvedMessage, duration: 30000, }); }, }; } + +function resolveErrorMessage( + props: OnSubmitErrorDataType | undefined, + override?: string | ((props: OnSubmitErrorDataType) => string) +): string { + if (override) { + if (typeof override === "string") { + return override; + } + if (props) { + return override(props); + } + } + + if (props instanceof UserFacingError) { + return formatUserFacingError(props, { + fallback: "Unknown error occurred. The error has been logged", + }); + } + + return "Unknown error occurred. The error has been logged"; +} From de2572ff8d25f55975da80b61ab0adbb1c6d11b0 Mon Sep 17 00:00:00 2001 From: Oksamies Date: Tue, 2 Dec 2025 22:23:27 +0200 Subject: [PATCH 4/4] Correct error handling related imports and types --- .../app/p/components/ReportPackage/ReportPackageForm.tsx | 3 ++- apps/cyberstorm-remix/app/p/packageEdit.tsx | 3 ++- apps/cyberstorm-remix/app/p/tabs/Wiki/WikiNewPage.tsx | 3 ++- apps/cyberstorm-remix/app/p/tabs/Wiki/WikiPageEdit.tsx | 3 ++- apps/cyberstorm-remix/app/settings/teams/Teams.tsx | 3 ++- .../app/settings/teams/team/tabs/Members/MemberAddForm.tsx | 3 ++- .../app/settings/teams/team/tabs/Profile/Profile.tsx | 3 ++- .../teams/team/tabs/ServiceAccounts/ServiceAccounts.tsx | 3 ++- .../app/settings/teams/team/tabs/Settings/Settings.tsx | 3 ++- .../cyberstorm-remix/app/settings/user/Account/Account.tsx | 4 ++-- apps/cyberstorm-remix/app/upload/upload.tsx | 7 +++++-- packages/ts-api-react-actions/src/ApiAction.tsx | 7 +++++-- packages/ts-api-react-actions/src/useApiAction.ts | 2 +- packages/ts-api-react/src/useApiCall.ts | 2 +- 14 files changed, 32 insertions(+), 17 deletions(-) diff --git a/apps/cyberstorm-remix/app/p/components/ReportPackage/ReportPackageForm.tsx b/apps/cyberstorm-remix/app/p/components/ReportPackage/ReportPackageForm.tsx index faaafcc2c..b46bcb4f5 100644 --- a/apps/cyberstorm-remix/app/p/components/ReportPackage/ReportPackageForm.tsx +++ b/apps/cyberstorm-remix/app/p/components/ReportPackage/ReportPackageForm.tsx @@ -11,6 +11,7 @@ import { import { type PackageListingReportRequestData, type RequestConfig, + UserFacingError, packageListingReport, } from "@thunderstore/thunderstore-api"; @@ -81,7 +82,7 @@ export function ReportPackageForm( PackageListingReportRequestData, Error, SubmitorOutput, - Error, + UserFacingError, InputErrors >({ inputs: formInputs, diff --git a/apps/cyberstorm-remix/app/p/packageEdit.tsx b/apps/cyberstorm-remix/app/p/packageEdit.tsx index cab99f62e..518a36d8a 100644 --- a/apps/cyberstorm-remix/app/p/packageEdit.tsx +++ b/apps/cyberstorm-remix/app/p/packageEdit.tsx @@ -24,6 +24,7 @@ import { DapperTs } from "@thunderstore/dapper-ts"; import { ApiError, type PackageListingUpdateRequestData, + UserFacingError, packageDeprecate, packageListingUpdate, packageUnlist, @@ -214,7 +215,7 @@ export default function PackageListing() { PackageListingUpdateRequestData, Error, SubmitorOutput, - Error, + UserFacingError, InputErrors >({ inputs: formInputs, diff --git a/apps/cyberstorm-remix/app/p/tabs/Wiki/WikiNewPage.tsx b/apps/cyberstorm-remix/app/p/tabs/Wiki/WikiNewPage.tsx index 32ea46589..063744821 100644 --- a/apps/cyberstorm-remix/app/p/tabs/Wiki/WikiNewPage.tsx +++ b/apps/cyberstorm-remix/app/p/tabs/Wiki/WikiNewPage.tsx @@ -19,6 +19,7 @@ import { import { classnames } from "@thunderstore/cyberstorm"; import { type PackageWikiPageCreateRequestData, + UserFacingError, postPackageWikiPageCreate, } from "@thunderstore/thunderstore-api"; @@ -111,7 +112,7 @@ export default function Wiki() { PackageWikiPageCreateRequestData, Error, SubmitorOutput, - Error, + UserFacingError, InputErrors >({ inputs: formInputs, diff --git a/apps/cyberstorm-remix/app/p/tabs/Wiki/WikiPageEdit.tsx b/apps/cyberstorm-remix/app/p/tabs/Wiki/WikiPageEdit.tsx index 151c89501..c30cd9bfc 100644 --- a/apps/cyberstorm-remix/app/p/tabs/Wiki/WikiPageEdit.tsx +++ b/apps/cyberstorm-remix/app/p/tabs/Wiki/WikiPageEdit.tsx @@ -28,6 +28,7 @@ import { type PackageWikiPageEditRequestData, type PackageWikiPageResponseData, type RequestConfig, + UserFacingError, deletePackageWikiPage, postPackageWikiPageEdit, } from "@thunderstore/thunderstore-api"; @@ -181,7 +182,7 @@ export default function WikiEdit() { PackageWikiPageEditRequestData, Error, SubmitorOutput, - Error, + UserFacingError, InputErrors >({ inputs: formInputs, diff --git a/apps/cyberstorm-remix/app/settings/teams/Teams.tsx b/apps/cyberstorm-remix/app/settings/teams/Teams.tsx index 903acdc13..294278851 100644 --- a/apps/cyberstorm-remix/app/settings/teams/Teams.tsx +++ b/apps/cyberstorm-remix/app/settings/teams/Teams.tsx @@ -20,6 +20,7 @@ import { postTeamCreate } from "@thunderstore/dapper-ts"; import { type RequestConfig, type TeamCreateRequestData, + UserFacingError, teamCreate, } from "@thunderstore/thunderstore-api"; import { @@ -161,7 +162,7 @@ function CreateTeamForm(props: { config: () => RequestConfig }) { TeamCreateRequestData, Error, SubmitorOutput, - Error, + UserFacingError, InputErrors >({ inputs: formInputs, diff --git a/apps/cyberstorm-remix/app/settings/teams/team/tabs/Members/MemberAddForm.tsx b/apps/cyberstorm-remix/app/settings/teams/team/tabs/Members/MemberAddForm.tsx index ac270744e..683a833cf 100644 --- a/apps/cyberstorm-remix/app/settings/teams/team/tabs/Members/MemberAddForm.tsx +++ b/apps/cyberstorm-remix/app/settings/teams/team/tabs/Members/MemberAddForm.tsx @@ -15,6 +15,7 @@ import { import { type RequestConfig, type TeamAddMemberRequestData, + UserFacingError, teamAddMember, } from "@thunderstore/thunderstore-api"; @@ -69,7 +70,7 @@ export function MemberAddForm(props: { TeamAddMemberRequestData, Error, SubmitorOutput, - Error, + UserFacingError, InputErrors >({ inputs: formInputs, diff --git a/apps/cyberstorm-remix/app/settings/teams/team/tabs/Profile/Profile.tsx b/apps/cyberstorm-remix/app/settings/teams/team/tabs/Profile/Profile.tsx index d237eb508..61c8c053e 100644 --- a/apps/cyberstorm-remix/app/settings/teams/team/tabs/Profile/Profile.tsx +++ b/apps/cyberstorm-remix/app/settings/teams/team/tabs/Profile/Profile.tsx @@ -13,6 +13,7 @@ import { NewButton, NewTextInput, useToast } from "@thunderstore/cyberstorm"; import { type TeamDetails, type TeamDetailsEditRequestData, + UserFacingError, teamDetailsEdit, } from "@thunderstore/thunderstore-api"; @@ -86,7 +87,7 @@ function ProfileForm(props: { team: TeamDetails }) { TeamDetailsEditRequestData, Error, SubmitorOutput, - Error, + UserFacingError, InputErrors >({ inputs: formInputs, diff --git a/apps/cyberstorm-remix/app/settings/teams/team/tabs/ServiceAccounts/ServiceAccounts.tsx b/apps/cyberstorm-remix/app/settings/teams/team/tabs/ServiceAccounts/ServiceAccounts.tsx index dbca8db3c..a0a60f207 100644 --- a/apps/cyberstorm-remix/app/settings/teams/team/tabs/ServiceAccounts/ServiceAccounts.tsx +++ b/apps/cyberstorm-remix/app/settings/teams/team/tabs/ServiceAccounts/ServiceAccounts.tsx @@ -22,6 +22,7 @@ import { } from "@thunderstore/cyberstorm"; import { type TeamServiceAccountAddRequestData, + UserFacingError, teamAddServiceAccount, } from "@thunderstore/thunderstore-api"; @@ -136,7 +137,7 @@ function AddServiceAccountForm(props: { TeamServiceAccountAddRequestData, Error, SubmitorOutput, - Error, + UserFacingError, InputErrors >({ inputs: formInputs, diff --git a/apps/cyberstorm-remix/app/settings/teams/team/tabs/Settings/Settings.tsx b/apps/cyberstorm-remix/app/settings/teams/team/tabs/Settings/Settings.tsx index 47e161e9f..7909b1f02 100644 --- a/apps/cyberstorm-remix/app/settings/teams/team/tabs/Settings/Settings.tsx +++ b/apps/cyberstorm-remix/app/settings/teams/team/tabs/Settings/Settings.tsx @@ -24,6 +24,7 @@ import { import { type RequestConfig, type TeamDisbandRequestData, + UserFacingError, teamDisband, teamRemoveMember, } from "@thunderstore/thunderstore-api"; @@ -257,7 +258,7 @@ function DisbandTeamForm(props: { TeamDisbandRequestData, Error, SubmitorOutput, - Error, + UserFacingError, InputErrors >({ inputs: formInputs, diff --git a/apps/cyberstorm-remix/app/settings/user/Account/Account.tsx b/apps/cyberstorm-remix/app/settings/user/Account/Account.tsx index 1dfa927e7..6c65a6fbe 100644 --- a/apps/cyberstorm-remix/app/settings/user/Account/Account.tsx +++ b/apps/cyberstorm-remix/app/settings/user/Account/Account.tsx @@ -15,7 +15,7 @@ import { NewTextInput, useToast, } from "@thunderstore/cyberstorm"; -import { userDelete } from "@thunderstore/thunderstore-api"; +import { UserFacingError, userDelete } from "@thunderstore/thunderstore-api"; import "./Account.css"; @@ -126,7 +126,7 @@ function DeleteAccountForm(props: { UserAccountDeleteRequestData, Error, SubmitorOutput, - Error, + UserFacingError, InputErrors >({ inputs: formInputs, diff --git a/apps/cyberstorm-remix/app/upload/upload.tsx b/apps/cyberstorm-remix/app/upload/upload.tsx index 682575871..765ac59fc 100644 --- a/apps/cyberstorm-remix/app/upload/upload.tsx +++ b/apps/cyberstorm-remix/app/upload/upload.tsx @@ -37,7 +37,10 @@ import { postPackageSubmissionMetadata, } from "@thunderstore/dapper-ts"; import { DnDFileInput } from "@thunderstore/react-dnd"; -import { type PackageSubmissionRequestData } from "@thunderstore/thunderstore-api"; +import { + type PackageSubmissionRequestData, + UserFacingError, +} from "@thunderstore/thunderstore-api"; import { type IBaseUploadHandle, MultipartUpload, @@ -332,7 +335,7 @@ export default function Upload() { PackageSubmissionRequestData, Error, SubmitorOutput, - Error, + UserFacingError, InputErrors >({ inputs: formInputs, diff --git a/packages/ts-api-react-actions/src/ApiAction.tsx b/packages/ts-api-react-actions/src/ApiAction.tsx index ccc385520..bb42e6d69 100644 --- a/packages/ts-api-react-actions/src/ApiAction.tsx +++ b/packages/ts-api-react-actions/src/ApiAction.tsx @@ -1,11 +1,14 @@ import { useCallback } from "react"; import { - ApiEndpointProps, + type ApiEndpointProps, UserFacingError, mapApiErrorToUserFacingError, } from "@thunderstore/thunderstore-api"; -import { ApiEndpoint, UseApiCallOptions } from "@thunderstore/ts-api-react"; +import { + type ApiEndpoint, + type UseApiCallOptions, +} from "@thunderstore/ts-api-react"; import { useApiAction } from "./useApiAction"; diff --git a/packages/ts-api-react-actions/src/useApiAction.ts b/packages/ts-api-react-actions/src/useApiAction.ts index 8752752a4..085eeb508 100644 --- a/packages/ts-api-react-actions/src/useApiAction.ts +++ b/packages/ts-api-react-actions/src/useApiAction.ts @@ -3,7 +3,7 @@ import { useCallback } from "react"; import { type ApiEndpointProps } from "@thunderstore/thunderstore-api"; import { type ApiEndpoint, - UseApiCallOptions, + type UseApiCallOptions, useApiCall, } from "@thunderstore/ts-api-react"; diff --git a/packages/ts-api-react/src/useApiCall.ts b/packages/ts-api-react/src/useApiCall.ts index 27f0a51e4..9548a8308 100644 --- a/packages/ts-api-react/src/useApiCall.ts +++ b/packages/ts-api-react/src/useApiCall.ts @@ -1,6 +1,6 @@ import { type ApiEndpointProps, - MapUserFacingErrorOptions, + type MapUserFacingErrorOptions, mapApiErrorToUserFacingError, } from "@thunderstore/thunderstore-api";