Skip to content

Commit 94f75d3

Browse files
committed
Wire up the submit button to propsNeedConfiguring
1 parent 6e5e297 commit 94f75d3

File tree

4 files changed

+114
-16
lines changed

4 files changed

+114
-16
lines changed

packages/connect-react/examples/nextjs/src/app/page.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ export default function Home() {
3030
componentKey="slack-send-message"
3131
configuredProps={configuredProps}
3232
onUpdateConfiguredProps={setConfiguredProps}
33+
onSubmit={async () => {
34+
await client.actionRun({
35+
userId,
36+
actionId: "slack-send-message",
37+
configuredProps,
38+
})}}
3339
/>
3440
</FrontendClientProvider>
3541
</>

packages/connect-react/src/components/ControlSubmit.tsx

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,22 @@ export type ControlSubmitProps = {
88

99
export function ControlSubmit(props: ControlSubmitProps) {
1010
const { form } = props;
11-
const { submitting } = form;
11+
const {
12+
propsNeedConfiguring, submitting,
13+
} = form;
1214

1315
const {
1416
getProps, theme,
1517
} = useCustomize();
16-
const baseStyles: CSSProperties = {
18+
const baseStyles = (disabled: boolean): CSSProperties => ({
1719
width: "fit-content",
1820
textTransform: "capitalize",
19-
backgroundColor: theme.colors.primary,
20-
color: theme.colors.neutral0,
21+
backgroundColor: disabled
22+
? theme.colors.neutral10
23+
: theme.colors.primary,
24+
color: disabled
25+
? theme.colors.neutral40
26+
: theme.colors.neutral0,
2127
padding: `${theme.spacing.baseUnit * 1.75}px ${
2228
theme.spacing.baseUnit * 16
2329
}px`,
@@ -29,9 +35,9 @@ export function ControlSubmit(props: ControlSubmitProps) {
2935
? 0.5
3036
: undefined,
3137
margin: "0.5rem 0 0 0",
32-
};
38+
});
3339

3440
return <input type="submit" value={submitting
3541
? "Submitting..."
36-
: "Submit"} {...getProps("controlSubmit", baseStyles, props)} disabled={submitting} />;
42+
: "Submit"} {...getProps("controlSubmit", baseStyles(propsNeedConfiguring.length || submitting), props)} disabled={propsNeedConfiguring.length || submitting} />;
3743
}

packages/connect-react/src/hooks/form-context.tsx

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ import {
44
import isEqual from "lodash.isequal";
55
import { useQuery } from "@tanstack/react-query";
66
import type {
7-
ComponentReloadPropsOpts, ConfigurableProp, ConfigurableProps, ConfiguredProps, V1Component,
7+
ComponentReloadPropsOpts, ConfigurableProp, ConfigurableProps, ConfiguredProps, V1Component, PropValue,
88
} from "@pipedream/sdk";
99
import { useFrontendClient } from "./frontend-client-context";
1010
import type { ComponentFormProps } from "../components/ComponentForm";
1111
import type { FormFieldContext } from "./form-field-context";
12+
import { appPropError } from "./use-app";
1213

1314
export type DynamicProps<T extends ConfigurableProps> = { id: string; configurable_props: T; }; // TODO
1415

@@ -24,6 +25,7 @@ export type FormContext<T extends ConfigurableProps> = {
2425
optionalPropIsEnabled: (prop: ConfigurableProp) => boolean;
2526
optionalPropSetEnabled: (prop: ConfigurableProp, enabled: boolean) => void;
2627
props: ComponentFormProps<T>;
28+
propsNeedConfiguring: string[];
2729
queryDisabledIdx?: number;
2830
registerField: <T extends ConfigurableProp>(field: FormFieldContext<T>) => void;
2931
setConfiguredProp: (idx: number, value: unknown) => void; // XXX type safety for value (T will rarely be static right?)
@@ -136,6 +138,16 @@ export const FormContextProvider = <T extends ConfigurableProps>({
136138
enabled: reloadPropIdx != null, // TODO or props.dynamicPropsId && !dynamicProps
137139
});
138140

141+
const [
142+
propsNeedConfiguring,
143+
setPropsNeedConfiguring,
144+
] = useState<string[]>([]);
145+
useEffect(() => {
146+
checkPropsNeedConfiguring()
147+
}, [
148+
configuredProps,
149+
]);
150+
139151
// XXX fix types of dynamicProps, props.component so this type decl not needed
140152
let configurableProps: T = dynamicProps?.configurable_props || formProps.component.configurable_props || [];
141153
if (propNames?.length) {
@@ -154,7 +166,7 @@ export const FormContextProvider = <T extends ConfigurableProps>({
154166

155167
// these validations are necessary because they might override PropInput for number case for instance
156168
// so can't rely on that base control form validation
157-
const propErrors = (prop: ConfigurableProp, value: unknown): string[] => {
169+
const propErrors = <T extends ConfigurableProps>(prop: ConfigurableProp, value: unknown): string[] => {
158170
const errs: string[] = [];
159171
if (value === undefined) {
160172
if (!prop.optional) {
@@ -181,13 +193,9 @@ export const FormContextProvider = <T extends ConfigurableProps>({
181193
}
182194
} else if (prop.type === "app") {
183195
const field = fields[prop.name]
184-
if (!field.extra.app) {
185-
errs.push("app field not registered")
186-
} else if (!value) {
187-
errs.push("no app configured")
188-
} else if (typeof value === "object" && "authProvisionId" in value && !value.authProvisionId) {
189-
errs.push("no auth provision configured")
190-
}
196+
const app = field.extra.app
197+
const err = appPropError(prop, value, app)
198+
if (err) errs.push(err)
191199
}
192200
return errs;
193201
};
@@ -316,6 +324,20 @@ export const FormContextProvider = <T extends ConfigurableProps>({
316324
setEnabledOptionalProps(newEnabledOptionalProps);
317325
};
318326

327+
const checkPropsNeedConfiguring = () => {
328+
const _propsNeedConfiguring = []
329+
for (const prop of configurableProps) {
330+
if (!prop || prop.optional || prop.hidden) continue
331+
const value = configuredProps[prop.name as keyof ConfiguredProps<T>]
332+
const errors = propErrors(prop, value)
333+
if (errors.length) {
334+
_propsNeedConfiguring.push(prop.name)
335+
}
336+
}
337+
// propsNeedConfiguring.splice(0, propsNeedConfiguring.length, ..._propsNeedConfiguring)
338+
setPropsNeedConfiguring(_propsNeedConfiguring)
339+
}
340+
319341
const registerField = <T extends ConfigurableProp>(field: FormFieldContext<T>) => {
320342
fields[field.prop.name] = field
321343
setFields(fields);
@@ -336,6 +358,7 @@ export const FormContextProvider = <T extends ConfigurableProps>({
336358
fields,
337359
optionalPropIsEnabled,
338360
optionalPropSetEnabled,
361+
propsNeedConfiguring,
339362
queryDisabledIdx,
340363
registerField,
341364
setConfiguredProp,

packages/connect-react/src/hooks/use-app.tsx

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ import {
22
useQuery, type UseQueryOptions,
33
} from "@tanstack/react-query";
44
import { useFrontendClient } from "./frontend-client-context";
5-
import type { AppRequestResponse } from "@pipedream/sdk";
5+
import type {
6+
AppRequestResponse, AppResponse, ConfigurablePropApp,
7+
PropValue,
8+
} from "@pipedream/sdk";
69

710
/**
811
* Get details about an app
@@ -23,3 +26,63 @@ export const useApp = (slug: string, opts?:{ useQueryOpts?: Omit<UseQueryOptions
2326
app: query.data?.data,
2427
};
2528
};
29+
30+
type AppResponseWithExtractedCustomFields = AppResponse & {
31+
extracted_custom_fields_names: string[]
32+
}
33+
34+
type AppCustomField = {
35+
name: string
36+
optional?: boolean
37+
}
38+
39+
type OauthAppPropValue = PropValue<"app"> & {
40+
oauth_access_token?: string
41+
}
42+
43+
function getCustomFields(app: AppResponse): AppCustomField[] {
44+
const isOauth = app.auth_type === "oauth"
45+
const userDefinedCustomFields = JSON.parse(app.custom_fields_json || "[]")
46+
if ("extracted_custom_fields_names" in app && app.extracted_custom_fields_names) {
47+
const extractedCustomFields = ((app as AppResponseWithExtractedCustomFields).extracted_custom_fields_names || []).map(
48+
(name) => ({
49+
name,
50+
}),
51+
)
52+
userDefinedCustomFields.push(...extractedCustomFields)
53+
}
54+
return userDefinedCustomFields.map((cf: AppCustomField) => {
55+
return {
56+
...cf,
57+
// if oauth, treat all as optional (they are usually needed for getting access token)
58+
optional: cf.optional || isOauth,
59+
}
60+
})
61+
}
62+
63+
export function appPropError(prop: ConfigurablePropApp, value: unknown, app: AppResponse | undefined): string | undefined {
64+
console.log("appPropError", prop, value, app)
65+
if (!app) {
66+
return "app field not registered"
67+
}
68+
if (!value) {
69+
return "no app configured"
70+
}
71+
if (typeof value !== "object") {
72+
return "not an app"
73+
}
74+
const _value = value as PropValue<"app">
75+
if ("authProvisionId" in _value && !_value.authProvisionId) {
76+
if (app.auth_type) {
77+
if (app.auth_type === "oauth" && !(_value as OauthAppPropValue).oauth_access_token) {
78+
return "missing oauth token"
79+
}
80+
for (const cf of getCustomFields(app)) {
81+
if (!cf.optional && !_value[cf.name]) {
82+
return "missing custom field"
83+
}
84+
}
85+
return "no auth provision configured"
86+
}
87+
}
88+
}

0 commit comments

Comments
 (0)