diff --git a/components/buysellads/buysellads.app.mjs b/components/buysellads/buysellads.app.mjs index 4ec7f20422624..c55456bf7ad31 100644 --- a/components/buysellads/buysellads.app.mjs +++ b/components/buysellads/buysellads.app.mjs @@ -8,4 +8,4 @@ export default { console.log(Object.keys(this.$auth)); }, }, -}; \ No newline at end of file +}; diff --git a/components/humanlayer/humanlayer.app.mjs b/components/humanlayer/humanlayer.app.mjs index e9be295001178..97cb1840555ed 100644 --- a/components/humanlayer/humanlayer.app.mjs +++ b/components/humanlayer/humanlayer.app.mjs @@ -8,4 +8,4 @@ export default { console.log(Object.keys(this.$auth)); }, }, -}; \ No newline at end of file +}; diff --git a/components/planhat/planhat.app.mjs b/components/planhat/planhat.app.mjs index c93c9f1f81b01..801bda0325eca 100644 --- a/components/planhat/planhat.app.mjs +++ b/components/planhat/planhat.app.mjs @@ -8,4 +8,4 @@ export default { console.log(Object.keys(this.$auth)); }, }, -}; \ No newline at end of file +}; diff --git a/components/storerocket/storerocket.app.mjs b/components/storerocket/storerocket.app.mjs index a5b1e804c9d27..b395b5698defc 100644 --- a/components/storerocket/storerocket.app.mjs +++ b/components/storerocket/storerocket.app.mjs @@ -8,4 +8,4 @@ export default { console.log(Object.keys(this.$auth)); }, }, -}; \ No newline at end of file +}; diff --git a/packages/connect-react/CHANGELOG.md b/packages/connect-react/CHANGELOG.md index 74bf96e59f6fe..dd7cc6f26bd54 100644 --- a/packages/connect-react/CHANGELOG.md +++ b/packages/connect-react/CHANGELOG.md @@ -1,6 +1,10 @@ # Changelog +# [1.0.0-preview.28] - 2025-02-05 + +- Surface SDK errors in the form + # [1.0.0-preview.27] - 2025-01-30 - Add styling to alerts diff --git a/packages/connect-react/README.md b/packages/connect-react/README.md index 8a82c3064bedb..4dee51914cc70 100644 --- a/packages/connect-react/README.md +++ b/packages/connect-react/README.md @@ -149,6 +149,11 @@ type ComponentFormProps = { onUpdateConfiguredProps: (v: Record) => void; /** Hide optional props section */ hideOptionalProps: boolean; + /** SDK response payload. Used in conjunction with enableDebugging to + * show errors in the form. */ + sdkResponse: unknown[] | unknown | undefined; + /** Whether to show show errors in the form. Requires sdkErrors to be set. */ + enableDebugging?: boolean; }; ``` diff --git a/packages/connect-react/examples/nextjs/package-lock.json b/packages/connect-react/examples/nextjs/package-lock.json index 602bed07c206e..70ee75eb7ff46 100644 --- a/packages/connect-react/examples/nextjs/package-lock.json +++ b/packages/connect-react/examples/nextjs/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.0", "dependencies": { "@pipedream/connect-react": "file:../..", - "@pipedream/sdk": "^1.1.4", + "@pipedream/sdk": "^1.3.2", "next": "15.0.3", "react": "19.0.0-rc-66855b96-20241106", "react-dom": "19.0.0-rc-66855b96-20241106" @@ -23,7 +23,7 @@ }, "../..": { "name": "@pipedream/connect-react", - "version": "1.0.0-preview.27", + "version": "1.0.0-preview.28", "license": "MIT", "dependencies": { "@pipedream/sdk": "workspace:^", @@ -53,47 +53,6 @@ "tslib": "^2.4.0" } }, - "node_modules/@hapi/boom": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/@hapi/boom/-/boom-10.0.1.tgz", - "integrity": "sha512-ERcCZaEjdH3OgSJlyjVk8pHIFeus91CjKP3v+MpgBNp5IvGzP2l/bRiD78nqYcKPaZdbKkK5vDBVPd2ohHBlsA==", - "dependencies": { - "@hapi/hoek": "^11.0.2" - } - }, - "node_modules/@hapi/bourne": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@hapi/bourne/-/bourne-3.0.0.tgz", - "integrity": "sha512-Waj1cwPXJDucOib4a3bAISsKJVb15MKi9IvmTI/7ssVEm6sywXGjVJDhl6/umt1pK1ZS7PacXU3A1PmFKHEZ2w==" - }, - "node_modules/@hapi/hoek": { - "version": "11.0.7", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-11.0.7.tgz", - "integrity": "sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==" - }, - "node_modules/@hapi/topo": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", - "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", - "dependencies": { - "@hapi/hoek": "^9.0.0" - } - }, - "node_modules/@hapi/topo/node_modules/@hapi/hoek": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", - "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==" - }, - "node_modules/@hapi/wreck": { - "version": "18.1.0", - "resolved": "https://registry.npmjs.org/@hapi/wreck/-/wreck-18.1.0.tgz", - "integrity": "sha512-0z6ZRCmFEfV/MQqkQomJ7sl/hyxvcZM7LtuVqN3vdAO4vM9eBbowl0kaqQj9EJJQab+3Uuh1GxbGIBFy4NfJ4w==", - "dependencies": { - "@hapi/boom": "^10.0.1", - "@hapi/bourne": "^3.0.0", - "@hapi/hoek": "^11.0.2" - } - }, "node_modules/@img/sharp-darwin-arm64": { "version": "0.33.5", "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", @@ -566,14 +525,14 @@ "link": true }, "node_modules/@pipedream/sdk": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@pipedream/sdk/-/sdk-1.1.5.tgz", - "integrity": "sha512-q+dGMsNSvIcDCi6JZES7pwNBW8Ms318WMNavO71Cye0bCtloeDSj5TiFbScy64/2FHja9keKAZ9ev8yVSsM+ew==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@pipedream/sdk/-/sdk-1.3.2.tgz", + "integrity": "sha512-50/eSDsR5fV2pyScgbAmfMiZ4kGxxvN2Zrg9RZa/Dt11VsQAcRNza1Vy8cGD6rc3luPeXwlGk+Diu0ynAtAKlA==", "license": "SEE LICENSE IN LICENSE", "dependencies": { "@rails/actioncable": "^8.0.0", "commander": "^12.1.0", - "simple-oauth2": "^5.1.0", + "oauth4webapi": "^3.1.4", "ws": "^8.18.0" }, "engines": { @@ -585,29 +544,6 @@ "resolved": "https://registry.npmjs.org/@rails/actioncable/-/actioncable-8.0.0.tgz", "integrity": "sha512-9IXyJeaBggOzlD3pF4/yWELdyUWZm/KTyKBRqxNf9laLBXPqxJt3t6fO+X4s0WajMR8cIhzkxvq1gxsXVbn3LA==" }, - "node_modules/@sideway/address": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", - "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", - "dependencies": { - "@hapi/hoek": "^9.0.0" - } - }, - "node_modules/@sideway/address/node_modules/@hapi/hoek": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", - "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==" - }, - "node_modules/@sideway/formula": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", - "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==" - }, - "node_modules/@sideway/pinpoint": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", - "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==" - }, "node_modules/@swc/counter": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", @@ -745,22 +681,6 @@ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "dev": true }, - "node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/detect-libc": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", @@ -776,28 +696,6 @@ "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", "optional": true }, - "node_modules/joi": { - "version": "17.13.3", - "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", - "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", - "dependencies": { - "@hapi/hoek": "^9.3.0", - "@hapi/topo": "^5.1.0", - "@sideway/address": "^4.1.5", - "@sideway/formula": "^3.0.1", - "@sideway/pinpoint": "^2.0.0" - } - }, - "node_modules/joi/node_modules/@hapi/hoek": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", - "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==" - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, "node_modules/nanoid": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", @@ -868,6 +766,15 @@ } } }, + "node_modules/oauth4webapi": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.1.4.tgz", + "integrity": "sha512-eVfN3nZNbok2s/ROifO0UAc5G8nRoLSbrcKJ09OqmucgnhXEfdIQOR4gq1eJH1rN3gV7rNw62bDEgftsgFtBEg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -975,17 +882,6 @@ "@img/sharp-win32-x64": "0.33.5" } }, - "node_modules/simple-oauth2": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/simple-oauth2/-/simple-oauth2-5.1.0.tgz", - "integrity": "sha512-gWDa38Ccm4MwlG5U7AlcJxPv3lvr80dU7ARJWrGdgvOKyzSj1gr3GBPN1rABTedAYvC/LsGYoFuFxwDBPtGEbw==", - "dependencies": { - "@hapi/hoek": "^11.0.4", - "@hapi/wreck": "^18.0.0", - "debug": "^4.3.4", - "joi": "^17.6.4" - } - }, "node_modules/simple-swizzle": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", diff --git a/packages/connect-react/examples/nextjs/package.json b/packages/connect-react/examples/nextjs/package.json index 02aa363bb7538..c520feccd93dd 100644 --- a/packages/connect-react/examples/nextjs/package.json +++ b/packages/connect-react/examples/nextjs/package.json @@ -7,7 +7,7 @@ }, "dependencies": { "@pipedream/connect-react": "file:../..", - "@pipedream/sdk": "^1.1.4", + "@pipedream/sdk": "^1.3.2", "next": "15.0.3", "react": "19.0.0-rc-66855b96-20241106", "react-dom": "19.0.0-rc-66855b96-20241106" diff --git a/packages/connect-react/examples/nextjs/src/app/page.tsx b/packages/connect-react/examples/nextjs/src/app/page.tsx index d3b1e1702bff8..2d51d322f4d9b 100644 --- a/packages/connect-react/examples/nextjs/src/app/page.tsx +++ b/packages/connect-react/examples/nextjs/src/app/page.tsx @@ -10,7 +10,6 @@ import { fetchToken } from "./actions"; export default function Home() { const userId = "my-authed-user-id"; const client = createFrontendClient({ - environment: "development", externalUserId: userId, tokenCallback: fetchToken, }); @@ -26,6 +25,11 @@ export default function Home() { setDynamicPropsId, ] = useState(); + const [ + sdkResponse, + setSdkResponse, + ] = useState(undefined); + const handleDynamicProps = (dynamicProps: { id: string | undefined }) => { setDynamicPropsId(dynamicProps.id) } @@ -40,14 +44,17 @@ export default function Home() { configuredProps={configuredProps} onUpdateDynamicProps={handleDynamicProps} onUpdateConfiguredProps={setConfiguredProps} + sdkResponse={sdkResponse} + enableDebugging={true} onSubmit={async () => { try { - await client.actionRun({ + const response = await client.runAction({ userId, actionId: "slack-send-message", configuredProps, dynamicPropsId, }); + setSdkResponse(response) } catch (error) { console.error("Action run failed:", error); } diff --git a/packages/connect-react/package.json b/packages/connect-react/package.json index a9b76237249a2..22638596162b7 100644 --- a/packages/connect-react/package.json +++ b/packages/connect-react/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/connect-react", - "version": "1.0.0-preview.27", + "version": "1.0.0-preview.28", "description": "Pipedream Connect library for React", "files": [ "dist" diff --git a/packages/connect-react/src/components/Alert.tsx b/packages/connect-react/src/components/Alert.tsx index 3d7bc2db3c6fc..968a177250dc7 100644 --- a/packages/connect-react/src/components/Alert.tsx +++ b/packages/connect-react/src/components/Alert.tsx @@ -7,8 +7,9 @@ type AlertProps = { export function Alert({ prop }: AlertProps) { const baseStyles = { + width: "100%", background: "#e2e3e5", - borderRadius: "10px", + borderRadius: "5px", paddingTop: "2px", paddingLeft: "10px", paddingRight: "10px", diff --git a/packages/connect-react/src/components/ComponentForm.tsx b/packages/connect-react/src/components/ComponentForm.tsx index e796c34c414f4..47c19f9cac8f2 100644 --- a/packages/connect-react/src/components/ComponentForm.tsx +++ b/packages/connect-react/src/components/ComponentForm.tsx @@ -20,6 +20,8 @@ export type ComponentFormProps void; // XXX onChange? onUpdateDynamicProps?: (dp: DynamicProps) => void; hideOptionalProps?: boolean; + sdkResponse?: unknown | undefined; + enableDebugging?: boolean; }; export function ComponentForm(props: ComponentFormProps) { diff --git a/packages/connect-react/src/components/Description.tsx b/packages/connect-react/src/components/Description.tsx index 0b8366d722ce3..abd04cdde795a 100644 --- a/packages/connect-react/src/components/Description.tsx +++ b/packages/connect-react/src/components/Description.tsx @@ -1,6 +1,8 @@ import type { CSSProperties } from "react"; import Markdown from "react-markdown"; -import { ConfigurableProp, ConfigurableProps } from "@pipedream/sdk"; +import { + ConfigurableProp, ConfigurableProps, +} from "@pipedream/sdk"; import { useCustomize } from "../hooks/customization-context"; import { FormFieldContext } from "../hooks/form-field-context"; import { FormContext } from "../hooks/form-context"; @@ -14,9 +16,6 @@ export type DescriptionProps(props: DescriptionProps) { - if (!props.field) { - console.log("props", props); - } const { field, markdown, } = props; diff --git a/packages/connect-react/src/components/Errors.tsx b/packages/connect-react/src/components/Errors.tsx index 1596a9480d304..0ebf460ffa3af 100644 --- a/packages/connect-react/src/components/Errors.tsx +++ b/packages/connect-react/src/components/Errors.tsx @@ -1,38 +1,50 @@ import { FormContext } from "../hooks/form-context"; import type { FormFieldContext } from "../hooks/form-field-context"; import { - ConfigurableProp, ConfigurableProps, + ConfigurableProp, ConfigurablePropAlert, ConfigurableProps, } from "@pipedream/sdk"; -import { useCustomize } from "../hooks/customization-context"; -import type { CSSProperties } from "react"; +import { Alert } from "./Alert"; export type ErrorsProps = { - errors: string[]; field: FormFieldContext; form: FormContext; }; export function Errors(props: ErrorsProps) { - const { errors } = props; - + const { field } = props; const { - getProps, theme, - } = useCustomize(); + errors = {}, prop = {}, enableDebugging, + } = field - const baseStyles: CSSProperties = { - color: theme.colors.danger, - gridArea: "errors", - }; + if (!enableDebugging) { + return null + } - if (!errors.length) { - return null; + if (!errors[prop.name]) { + return null } // TODO depending on type does different shit... we might need async loader around the label, etc.? // maybe that should just be handled by FormFieldContext instead of container? + + const formattedErrors: ConfigurablePropAlert[] = errors[prop.name].map((e) => { + return { + type: "alert", + alertType: "error", + content: e, + } + }) + + const baseStyles = { + display: "grid", + gridTemplateColumns: "max-content", + } + + const FormattedErrors = () => { + return <>{formattedErrors.map((fe, idx: number) => )} + } + return ( -
    - {errors.map((msg) =>
  • {msg}
  • )} -
+
); } diff --git a/packages/connect-react/src/components/Field.tsx b/packages/connect-react/src/components/Field.tsx index 31a553bb9946e..284055797c86f 100644 --- a/packages/connect-react/src/components/Field.tsx +++ b/packages/connect-react/src/components/Field.tsx @@ -40,8 +40,6 @@ export function Field(props: FieldProps) { Label, Description, Errors, } = getComponents(); - const errors: string[] = []; // TODO get from field context, can be added or removed by Control, etc. - const app = "app" in field.extra ? field.extra.app : undefined; @@ -59,12 +57,13 @@ export function Field(props: FieldProps) { // maybe that should just be handled by FormFieldContext instead of container? // XXX rename to FieldErrors + add FormErrors (to ComponentFormInternal) // XXX use similar pattern as app below for boolean and checkboxing DOM re-ordering? + return (
); } diff --git a/packages/connect-react/src/components/InternalComponentForm.tsx b/packages/connect-react/src/components/InternalComponentForm.tsx index e890b79adc869..2257c5f8087b3 100644 --- a/packages/connect-react/src/components/InternalComponentForm.tsx +++ b/packages/connect-react/src/components/InternalComponentForm.tsx @@ -1,4 +1,6 @@ -import { Suspense } from "react"; +import { + Suspense, useEffect, useState, +} from "react"; import type { CSSProperties, FormEventHandler, } from "react"; @@ -11,7 +13,13 @@ import { InternalField } from "./InternalField"; import { Alert } from "./Alert"; import { ErrorBoundary } from "./ErrorBoundary"; import { ControlSubmit } from "./ControlSubmit"; -import type { ConfigurableProp } from "@pipedream/sdk"; +import type { + ConfigurableProp, ConfigurablePropAlert, +} from "@pipedream/sdk"; + +const alwaysShowSdkErrors = [ + "ConfigurationError", +] export function InternalComponentForm() { const formContext = useFormContext(); @@ -23,12 +31,40 @@ export function InternalComponentForm() { optionalPropSetEnabled, props: formContextProps, setSubmitting, + sdkErrors: __sdkErrors, + submitting, + enableDebugging, } = formContext; + const showSdkErrors = enableDebugging || __sdkErrors.filter((e) => alwaysShowSdkErrors.includes(e.name)).length > 0 + const { hideOptionalProps, onSubmit, } = formContextProps; + const [ + sdkErrors, + setSdkErrors, + ] = useState([]) + + useEffect(() => { + if (submitting) setSdkErrors([]) + else { + if (__sdkErrors && __sdkErrors.length) { + setSdkErrors(__sdkErrors.map((e) => { + return { + type: "alert", + alertType: "error", + content: `# ${e.name}\n${e.message}`, + } as ConfigurablePropAlert + })) + } + } + }, [ + __sdkErrors, + submitting, + ]); + const { getComponents, getProps, theme, } = useCustomize(); @@ -96,6 +132,7 @@ export function InternalComponentForm() { } // TODO improve the error boundary thing (use default Alert component maybe) + return (

: null} + { showSdkErrors && sdkErrors?.map((e, idx) => )} {onSubmit && } diff --git a/packages/connect-react/src/components/InternalField.tsx b/packages/connect-react/src/components/InternalField.tsx index 047d714914891..24081bf818a73 100644 --- a/packages/connect-react/src/components/InternalField.tsx +++ b/packages/connect-react/src/components/InternalField.tsx @@ -15,7 +15,7 @@ export function InternalField({ }: FieldInternalProps) { const formCtx = useFormContext(); const { - id: formId, configuredProps, registerField, setConfiguredProp, + id: formId, configuredProps, registerField, setConfiguredProp, errors, enableDebugging, } = formCtx; const appSlug = prop.type === "app" && "app" in prop @@ -44,6 +44,8 @@ export function InternalField({ extra: { app, // XXX fix ts }, + errors, + enableDebugging, }; useEffect(() => registerField(fieldCtx), [ fieldCtx, diff --git a/packages/connect-react/src/hooks/form-context.tsx b/packages/connect-react/src/hooks/form-context.tsx index 2f26376e4b3dc..0c9ed391c6927 100644 --- a/packages/connect-react/src/hooks/form-context.tsx +++ b/packages/connect-react/src/hooks/form-context.tsx @@ -14,6 +14,9 @@ import { stringPropErrors, } from "../utils/component"; import _ from "lodash"; +import { + Observation, SdkError, +} from "../types"; export type DynamicProps = { id: string; configurableProps: T; }; // TODO @@ -24,6 +27,7 @@ export type FormContext = { dynamicProps?: DynamicProps; // lots of calls require dynamicProps?.id, so need to expose dynamicPropsQueryIsFetching?: boolean; errors: Record; + sdkErrors: SdkError[]; fields: Record>; id: string; isValid: boolean; @@ -37,6 +41,7 @@ export type FormContext = { setSubmitting: (submitting: boolean) => void; submitting: boolean; userId: string; + enableDebugging: boolean; }; export const skippablePropTypes = [ @@ -73,7 +78,7 @@ export const FormContextProvider = ({ const id = useId(); const { - component, configuredProps: __configuredProps, propNames, userId, + component, configuredProps: __configuredProps, propNames, userId, sdkResponse, enableDebugging: __enableDebugging, } = formProps; const componentId = component.key; @@ -94,6 +99,16 @@ export const FormContextProvider = ({ setErrors, ] = useState>({}); + const [ + sdkErrors, + setSdkErrors, + ] = useState([]) + + const [ + enableDebugging + , + ] = useState(__enableDebugging === true) + const [ enabledOptionalProps, setEnabledOptionalProps, @@ -143,7 +158,18 @@ export const FormContextProvider = ({ queryKeyInput, ], queryFn: async () => { - const { dynamicProps } = await client.componentReloadProps(componentReloadPropsInput); + const result = await client.componentReloadProps(componentReloadPropsInput); + const { + dynamicProps, observations, errors: __errors, + } = result + + // Prioritize errors from observations over the errors array + if (observations && observations.filter((o) => o.k === "error").length > 0) { + handleSdkErrors(observations) + } else { + handleSdkErrors(__errors) + } + // XXX what about if null? // TODO observation errors, etc. if (dynamicProps) { @@ -243,6 +269,10 @@ export const FormContextProvider = ({ const updateConfiguredProps = (configuredProps: ConfiguredProps) => { setConfiguredProps(configuredProps); updateConfiguredPropsQueryDisabledIdx(configuredProps); + updateConfigurationErrors(configuredProps) + }; + + const updateConfigurationErrors = (configuredProps: ConfiguredProps) => { const _errors: typeof errors = {}; for (let idx = 0; idx < configurableProps.length; idx++) { const prop = configurableProps[idx]; @@ -264,6 +294,20 @@ export const FormContextProvider = ({ _configuredProps, ]); + useEffect(() => { + updateConfigurationErrors(configuredProps) + }, [ + configuredProps, + reloadPropIdx, + queryDisabledIdx, + ]); + + useEffect(() => { + handleSdkErrors(sdkResponse) + }, [ + sdkResponse, + ]); + useEffect(() => { const newConfiguredProps: ConfiguredProps = {}; for (const prop of configurableProps) { @@ -395,6 +439,108 @@ export const FormContextProvider = ({ checkPropsNeedConfiguring() }; + const handleSdkErrors = (sdkResponse: unknown[] | unknown | undefined) => { + if (!sdkResponse) return + + let newErrors = [ + ...sdkErrors, + ] + + const errorFromString = (item: string, ret: SdkError[]) => { + try { + const json = JSON.parse(item) + const err: SdkError = { + name: json.name, + message: json.message, + } + if (err.name && err.message) { + ret.push(err) + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (e) { + // pass + } + } + + const errorFromObject = (item: unknown, ret: SdkError[]) => { + const err: SdkError = { + name: item.name, + message: item.message, + } + if (err.name && err.message) { + ret.push(err) + } + } + + const errorFromObservationError = (item: Error, ret: SdkError[]) => { + const err: SdkError = { + name: item.err?.name, + message: item.err?.message, + } + if (err.name && err.message) { + ret.push(err) + } + } + + const errorFromObservation = (payload: Observation, ret: SdkError[]) => { + const os = payload.os || payload.observations + if (Array.isArray(os) && os.length > 0) { + for (let i = 0; i < os.length; i++) { + if (os[i].k !== "error") continue + errorFromObservationError(os[i], ret) + } + } + } + + const errorFromDetails = (data: unknown, ret: SdkError[]) => { + ret.push({ + name: data.error, + message: JSON.stringify(data.details), + // message: ` // TODO: It would be nice to render the JSON in markdown + // \`\`\`json + // ${JSON.stringify(data.details)} + // \`\`\` + // `, + // }) + }) + } + + const errorFromHttpError = (payload: Error, ret: SdkError[]) => { + // Handle HTTP errors thrown by the SDK + try { + const data = JSON.parse(payload.message)?.data + if (data && "observations" in data) { + errorFromObservation(data, ret) + } else if (data && "error" in data && "details" in data) { + errorFromDetails(data, ret) + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (e) { + // pass + } + } + + if (Array.isArray(sdkResponse) && sdkResponse.length > 0) { + for (let i = 0; i < sdkResponse.length; i++) { + const item = sdkResponse[i] + if (typeof item === "string") { + errorFromString(item, newErrors) + } else if (typeof item === "object" && "name" in item && "message" in item) { + errorFromObject(item, newErrors) + } else if (typeof item === "object" && item.k === "error") { + errorFromObservationError(item, newErrors) + } + } + } else if (typeof sdkResponse === "object" && "os" in sdkResponse || "observations" in sdkResponse) { + errorFromObservation(sdkResponse, newErrors) + } else if (typeof sdkResponse === "object" && "message" in sdkResponse) { + errorFromHttpError(sdkResponse, newErrors) + } else { + newErrors = [] + } + setSdkErrors(newErrors) + } + // console.log("***", configurableProps, configuredProps) const value: FormContext = { id, @@ -416,6 +562,8 @@ export const FormContextProvider = ({ setConfiguredProp, setSubmitting, submitting, + sdkErrors, + enableDebugging, }; return {children}; }; diff --git a/packages/connect-react/src/hooks/form-field-context.tsx b/packages/connect-react/src/hooks/form-field-context.tsx index bb05d46d3ddcb..9fd3abefb491c 100644 --- a/packages/connect-react/src/hooks/form-field-context.tsx +++ b/packages/connect-react/src/hooks/form-field-context.tsx @@ -16,6 +16,8 @@ export type FormFieldContext = { value: PropValue | undefined; onChange: (value: PropValue | undefined) => void; extra: FormFieldContextExtra; + errors: Record; + enableDebugging?: boolean; }; export const FormFieldContext = createContext | undefined>(undefined); // eslint-disable-line @typescript-eslint/no-explicit-any diff --git a/packages/connect-react/src/types.ts b/packages/connect-react/src/types.ts new file mode 100644 index 0000000000000..cae72eb2cc1c0 --- /dev/null +++ b/packages/connect-react/src/types.ts @@ -0,0 +1,11 @@ +export type SdkError = { + name: string; + message: string; +} + +export type Observation = { + ts: number; + k: string; + h?: string; + err?: Error; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d66742efa5ea7..7986be6b300cc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1522,8 +1522,7 @@ importers: specifier: ^0.1.4 version: 0.1.6 - components/buysellads: - specifiers: {} + components/buysellads: {} components/bybit: dependencies: @@ -5697,8 +5696,7 @@ importers: specifier: ^3.0.3 version: 3.0.3 - components/klipy: - specifiers: {} + components/klipy: {} components/knack: dependencies: @@ -12744,8 +12742,8 @@ importers: specifier: file:../.. version: file:packages/connect-react(@types/react@18.3.12)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) '@pipedream/sdk': - specifier: ^1.1.4 - version: 1.1.5 + specifier: ^1.3.2 + version: 1.3.2 next: specifier: 15.0.3 version: 15.0.3(@babel/core@8.0.0-alpha.13)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) @@ -15633,8 +15631,8 @@ packages: resolution: {integrity: sha512-f8FXEaoBqIOQpI4vVOO8OrHRAjwQYO4pKIwMUcGRkzS5KJ6cZ/7JYYGyXEu7NcS+y1VfyegtbJWu4tWacDjQZg==} engines: {node: '>=18.0.0'} - '@pipedream/sdk@1.1.5': - resolution: {integrity: sha512-q+dGMsNSvIcDCi6JZES7pwNBW8Ms318WMNavO71Cye0bCtloeDSj5TiFbScy64/2FHja9keKAZ9ev8yVSsM+ew==} + '@pipedream/sdk@1.3.2': + resolution: {integrity: sha512-50/eSDsR5fV2pyScgbAmfMiZ4kGxxvN2Zrg9RZa/Dt11VsQAcRNza1Vy8cGD6rc3luPeXwlGk+Diu0ynAtAKlA==} engines: {node: '>=18.0.0'} '@pipedream/sftp@0.4.1': @@ -31794,15 +31792,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@pipedream/sdk@1.1.5': + '@pipedream/sdk@1.3.2': dependencies: '@rails/actioncable': 8.0.0 commander: 12.1.0 - simple-oauth2: 5.1.0 + oauth4webapi: 3.1.4 ws: 8.18.0 transitivePeerDependencies: - bufferutil - - supports-color - utf-8-validate '@pipedream/sftp@0.4.1':