Skip to content

Commit f6b1b4a

Browse files
committed
use a single select when using related models in AutoForm
1 parent af2b74e commit f6b1b4a

26 files changed

+705
-697
lines changed

packages/react/cypress/component/auto/form/AutoFormFindByObjects.cy.tsx

+11-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,12 @@ describeForEachAutoAdapter("AutoForm - FindBy object parameters", ({ name, adapt
4040
mainModel: {
4141
childModelEntries: null,
4242
nonUniqueString: "example",
43-
uniqueBelongsTo: {},
43+
uniqueBelongsTo: {
44+
update: {
45+
id: "22",
46+
parentUniqueString: "parent-example",
47+
},
48+
},
4449
uniqueEmail: "[email protected]",
4550
uniqueString: "u2",
4651
},
@@ -411,6 +416,11 @@ const mainModelQueryDefaultValuesResponse = {
411416
nonUniqueString: "example",
412417
uniqueEmail: "[email protected]",
413418
uniqueString: "u2",
419+
uniqueBelongsTo: {
420+
__typename: "UniqueFieldsParentModel",
421+
id: "22",
422+
parentUniqueString: "parent-example",
423+
},
414424
updatedAt: "2024-10-01T20:58:39.300Z",
415425
},
416426
__typename: "UniqueFieldsMainModelEdge",

packages/react/cypress/component/auto/form/AutoFormHasManyThrough.cy.tsx

+27-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ describeForEachAutoAdapter("AutoForm - HasManyThrough fields", ({ name, adapter:
2020
it("does not render the hasMany->joinModel input field", () => {
2121
interceptModelActionMetadataRequest();
2222

23-
cy.mountWithWrapper(<AutoForm action={api.widget.create} />, wrapper);
23+
cy.mountWithWrapper(<AutoForm action={api.hasManyThrough.baseModel.create} />, wrapper);
2424
cy.wait("@ModelActionMetadata");
2525

2626
// Name field input is shown
@@ -149,6 +149,32 @@ const modelActionMetadataResponse = {
149149
},
150150
__typename: "GadgetModel",
151151
},
152+
{
153+
key: "tJDsf_FvYqsi",
154+
apiIdentifier: "joinerModel",
155+
namespace: ["hasManyThrough"],
156+
defaultDisplayField: {
157+
name: "Id",
158+
apiIdentifier: "id",
159+
fieldType: "ID",
160+
__typename: "GadgetModelField",
161+
},
162+
fields: [],
163+
__typename: "GadgetModel",
164+
},
165+
{
166+
key: "Oss4sCDW-DJU",
167+
apiIdentifier: "siblingModel",
168+
namespace: ["hasManyThrough"],
169+
defaultDisplayField: {
170+
name: "Id",
171+
apiIdentifier: "id",
172+
fieldType: "ID",
173+
__typename: "GadgetModelField",
174+
},
175+
fields: [],
176+
__typename: "GadgetModel",
177+
},
152178
],
153179
model: {
154180
name: "Base model",

packages/react/cypress/component/auto/form/AutoFormUpsertAction.cy.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ describeForEachAutoAdapter("AutoForm - Upsert Action", ({ name, adapter: { AutoF
147147
cy.contains("Record Not Found Error: Gadget API returned no data at widget").should("exist");
148148
});
149149

150-
it("Can properly submit with custom form contents", () => {
150+
it.only("Can properly submit with custom form contents", () => {
151151
mockSuccessfulWidgetFindBy();
152152
mockSuccessfulUpsert();
153153

packages/react/cypress/support/component.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ before(() => {
6767

6868
beforeEach(() => {
6969
cy.window().then((win) => {
70+
if (!win) return;
7071
const mockToasts = win.document.getElementsByClassName("mock-toast");
7172
while (mockToasts.length > 0) {
7273
try {

packages/react/spec/auto/PolarisAutoForm.spec.tsx

+7-7
Original file line numberDiff line numberDiff line change
@@ -719,12 +719,6 @@ function loadMockGizmoCreateMetadata() {
719719
}
720720

721721
function loadMockWidgetCreateMetadata(opts?: { inputFields?: any[]; triggers?: any[] }) {
722-
expect(mockUrqlClient.executeQuery.mock.calls[0][0].variables).toEqual({
723-
modelApiIdentifier: "widget",
724-
modelNamespace: null,
725-
action: "create",
726-
});
727-
728722
mockUrqlClient.executeQuery.pushResponse("ModelActionMetadata", {
729723
stale: false,
730724
hasNext: false,
@@ -738,6 +732,12 @@ function loadMockWidgetCreateMetadata(opts?: { inputFields?: any[]; triggers?: a
738732
opts?.triggers
739733
),
740734
});
735+
736+
expect(mockUrqlClient.executeQuery.mock.calls[0][0].variables).toEqual({
737+
modelApiIdentifier: "widget",
738+
modelNamespace: null,
739+
action: "create",
740+
});
741741
}
742742

743743
function loadMockWidgetUpdateMetadata() {
@@ -747,8 +747,8 @@ function loadMockWidgetUpdateMetadata() {
747747

748748
function loadMockWidgetUpdateMetadataWithFindBy() {
749749
mockWidgetUpdateHelperFunctions.expectMetadataRequest();
750-
mockWidgetUpdateHelperFunctions.mockFindByResponse();
751750
mockWidgetUpdateHelperFunctions.mockMetadataResponse();
751+
mockWidgetUpdateHelperFunctions.mockFindByResponse();
752752
}
753753

754754
const mockWidgetUpdateHelperFunctions = {

packages/react/spec/auto/inputs/PolarisAutoTextInput.spec.tsx

+20-20
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,26 @@ const metadata = {
332332
};
333333

334334
const mockFindBy = () => {
335+
const updateMetadata = { ...metadata, action: { ...metadata.action, apiIdentifier: "update", operatesWithRecordIdentity: true } };
336+
mockUrqlClient.executeQuery.pushResponse("ModelActionMetadata", {
337+
stale: false,
338+
hasNext: false,
339+
data: {
340+
gadgetMeta: {
341+
modelAndRelatedModels: [
342+
{
343+
name: "Widget",
344+
apiIdentifier: "widget",
345+
fields: updateMetadata.action.inputFields,
346+
__typename: "GadgetModel",
347+
},
348+
],
349+
model: updateMetadata,
350+
__typename: "GadgetApplicationMeta",
351+
},
352+
},
353+
});
354+
335355
mockUrqlClient.executeQuery.pushResponse("widget", {
336356
stale: false,
337357
hasNext: false,
@@ -361,24 +381,4 @@ const mockFindBy = () => {
361381
},
362382
},
363383
});
364-
365-
const updateMetadata = { ...metadata, action: { ...metadata.action, apiIdentifier: "update", operatesWithRecordIdentity: true } };
366-
mockUrqlClient.executeQuery.pushResponse("ModelActionMetadata", {
367-
stale: false,
368-
hasNext: false,
369-
data: {
370-
gadgetMeta: {
371-
modelAndRelatedModels: [
372-
{
373-
name: "Widget",
374-
apiIdentifier: "widget",
375-
fields: updateMetadata.action.inputFields,
376-
__typename: "GadgetModel",
377-
},
378-
],
379-
model: updateMetadata,
380-
__typename: "GadgetApplicationMeta",
381-
},
382-
},
383-
});
384384
};

packages/react/spec/auto/support/helper.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -30,16 +30,16 @@ export const mockWidgetFindBy = (
3030
action: Parameters<typeof getWidgetModelMetadata>[0],
3131
overridesRecord?: Parameters<typeof getWidgetRecord>[0]
3232
) => {
33-
mockUrqlClient.executeQuery.pushResponse("widget", {
33+
mockUrqlClient.executeQuery.pushResponse("ModelActionMetadata", {
3434
stale: false,
3535
hasNext: false,
36-
data: getWidgetRecord(overridesRecord),
36+
data: getWidgetModelMetadata(action),
3737
});
3838

39-
mockUrqlClient.executeQuery.pushResponse("ModelActionMetadata", {
39+
mockUrqlClient.executeQuery.pushResponse("widget", {
4040
stale: false,
4141
hasNext: false,
42-
data: getWidgetModelMetadata(action),
42+
data: getWidgetRecord(overridesRecord),
4343
});
4444
};
4545

packages/react/src/auto/AutoForm.ts

+75-11
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import type { ActionFunction, GadgetRecord, GlobalActionFunction } from "@gadgetinc/api-client-core";
1+
import type { ActionFunction, FieldSelection, GadgetRecord, GlobalActionFunction } from "@gadgetinc/api-client-core";
22
import { yupResolver } from "@hookform/resolvers/yup";
33
import type { ReactNode } from "react";
4-
import { useEffect, useMemo, useRef } from "react";
4+
import React, { useEffect, useMemo, useRef } from "react";
55
import type { AnyActionWithId, RecordIdentifier, UseActionFormHookStateData } from "src/use-action-form/types.js";
66
import type { GadgetObjectFieldConfig } from "../internal/gql/graphql.js";
77
import type { FieldMetadata, GlobalActionMetadata, ModelWithOneActionMetadata } from "../metadata.js";
8-
import { FieldType, filterAutoFormFieldList, isModelActionMetadata, useActionMetadata } from "../metadata.js";
8+
import { FieldType, buildAutoFormFieldList, isModelActionMetadata, useActionMetadata } from "../metadata.js";
9+
import { pathListToSelection } from "../use-table/helpers.js";
910
import type { FieldErrors, FieldValues } from "../useActionForm.js";
1011
import { useActionForm } from "../useActionForm.js";
1112
import { get, getFlattenedObjectKeys, type OptionsType } from "../utils.js";
@@ -16,6 +17,7 @@ import {
1617
validateTriggersFromApiClient,
1718
validateTriggersFromMetadata,
1819
} from "./AutoFormActionValidators.js";
20+
import { isAutoInput } from "./AutoInput.js";
1921

2022
/** The props that any <AutoForm/> component accepts */
2123
export type AutoFormProps<
@@ -89,22 +91,22 @@ export const useFormFields = (
8991
: [];
9092
const nonObjectFields = action.inputFields.filter((field) => field.configuration.__typename !== "GadgetObjectFieldConfig");
9193

92-
const includedRootLevelFields = filterAutoFormFieldList(nonObjectFields, options as any).map(
93-
(field) =>
94+
const includedRootLevelFields = buildAutoFormFieldList(nonObjectFields, options as any).map(
95+
([path, field]) =>
9496
({
95-
path: field.apiIdentifier,
97+
path,
9698
metadata: field,
9799
} as const)
98100
);
99101

100102
const includedObjectFields = objectFields.flatMap((objectField) =>
101-
filterAutoFormFieldList((objectField.configuration as unknown as GadgetObjectFieldConfig).fields as any, {
103+
buildAutoFormFieldList((objectField.configuration as unknown as GadgetObjectFieldConfig).fields as any, {
102104
...(options as any),
103105
isUpsertAction: true, // For upsert meta-actions, we allow IDs, and they are object fields instead of root level
104106
}).map(
105-
(innerField) =>
107+
([innerPath, innerField]) =>
106108
({
107-
path: `${objectField.apiIdentifier}.${innerField.apiIdentifier}`,
109+
path: `${objectField.apiIdentifier}.${innerPath}`,
108110
metadata: innerField,
109111
} as const)
110112
)
@@ -120,6 +122,19 @@ export const useFormFields = (
120122
}, [metadata, options]);
121123
};
122124

125+
export const useFormSelection = (
126+
modelApiIdentifier: string | undefined,
127+
fields: readonly { path: string; metadata: FieldMetadata }[]
128+
): FieldSelection | undefined => {
129+
if (!modelApiIdentifier) return;
130+
if (!fields.length) return;
131+
132+
const paths = fields.map((f) => f.path.replace(new RegExp(`^${modelApiIdentifier}\\.`), ""));
133+
const fieldMetaData = fields.map((f) => f.metadata);
134+
135+
return pathListToSelection(paths, fieldMetaData);
136+
};
137+
123138
const validateFormFieldApiIdentifierUniqueness = (actionApiIdentifier: string, inputApiIdentifiers: string[]) => {
124139
const seen = new Set<string>();
125140

@@ -142,7 +157,15 @@ export const useAutoForm = <
142157
>(
143158
props: AutoFormProps<GivenOptions, SchemaT, ActionFunc, any, any> & { findBy?: any }
144159
) => {
145-
const { action, record, onSuccess, onFailure, findBy } = props;
160+
const { action, record, onSuccess, onFailure, findBy, children } = props;
161+
162+
let include = props.include;
163+
let exclude = props.exclude;
164+
165+
if (children) {
166+
include = extractPathsFromChildren(children);
167+
exclude = undefined;
168+
}
146169

147170
validateNonBulkAction(action);
148171
validateTriggersFromApiClient(action);
@@ -152,12 +175,13 @@ export const useAutoForm = <
152175
validateTriggersFromMetadata(metadata);
153176

154177
// filter down the fields to render only what we want to render for this form
155-
const fields = useFormFields(metadata, props);
178+
const fields = useFormFields(metadata, { include, exclude });
156179
validateFindByObjectWithMetadata(fields, findBy);
157180
const isDeleteAction = metadata && isModelActionMetadata(metadata) && metadata.action.isDeleteAction;
158181
const isGlobalAction = action.type === "globalAction";
159182
const operatesWithRecordId = !!(metadata && isModelActionMetadata(metadata) && metadata.action.operatesWithRecordIdentity);
160183
const modelApiIdentifier = action.type == "action" ? action.modelApiIdentifier : undefined;
184+
const selection = useFormSelection(modelApiIdentifier, fields);
161185
const isUpsertMetaAction =
162186
metadata && isModelActionMetadata(metadata) && fields.some((field) => field.metadata.fieldType === FieldType.Id);
163187
const isUpsertWithFindBy = isUpsertMetaAction && !!findBy;
@@ -201,6 +225,8 @@ export const useAutoForm = <
201225
defaultValues: defaultValues as any,
202226
findBy: "findBy" in props ? props.findBy : undefined,
203227
throwOnInvalidFindByObject: false,
228+
pause: "findBy" in props ? fetchingMetadata : undefined,
229+
select: selection as any,
204230
resolver: useValidationResolver(metadata, fieldPathsToValidate),
205231
send: () => {
206232
const fieldsToSend = fields
@@ -282,6 +308,44 @@ export const useAutoForm = <
282308
};
283309
};
284310

311+
const extractPathsFromChildren = (children: React.ReactNode) => {
312+
const paths = new Set<string>();
313+
314+
React.Children.forEach(children, (child) => {
315+
if (React.isValidElement(child)) {
316+
const grandChildren = child.props.children as React.ReactNode | undefined;
317+
let childPaths: string[] = [];
318+
319+
if (grandChildren) {
320+
childPaths = extractPathsFromChildren(grandChildren);
321+
}
322+
323+
let field: string | undefined = undefined;
324+
325+
if (isAutoInput(child)) {
326+
const props = child.props as { field: string; selectPaths?: string[]; children?: React.ReactNode };
327+
field = props.field;
328+
329+
paths.add(field);
330+
331+
if (props.selectPaths && Array.isArray(props.selectPaths)) {
332+
props.selectPaths.forEach((selectPath) => {
333+
paths.add(`${field}.${selectPath}`);
334+
});
335+
}
336+
}
337+
338+
if (childPaths.length > 0) {
339+
for (const childPath of childPaths) {
340+
paths.add(field ? `${field}.${childPath}` : childPath);
341+
}
342+
}
343+
}
344+
});
345+
346+
return Array.from(paths);
347+
};
348+
285349
const removeIdFieldsUnlessUpsertWithoutFindBy = (isUpsertWithFindBy?: boolean) => {
286350
return (field: { metadata: FieldMetadata }) => {
287351
return field.metadata.fieldType === FieldType.Id ? !isUpsertWithFindBy : true;

0 commit comments

Comments
 (0)