Skip to content

Commit 5c7342f

Browse files
committed
Fix support for invoking bulk actions with flattened params
Support for invoking bulk actions with flattened params in react was broken, and #267 accidentally broke it further. It was broken since inception because the tests were asserting incorrectly that the produced `input` params weren't nested -- they should have been, oops! Updated the tests and stopped using snapshot tests to describe this so no one is tempted to break it. Then, to fix the issue and properly disambiguate the params, I had to change the behaviour of the disambiguator a little bit. To preserve the behaviour for the change in #267, we need to respect the idea that if an action does not accept model input, we shouldn't do any nesting of variables. This does this by giving the new `action.acceptsModelInput` value precedence over `action.hasCreateOrUpdateEffect`, which was the ancient way of asking this question. That old way keeps support for old API clients, but the new way is more trustworthy. For things like the signUp action, `action.acceptsModelInput` is false, but `action.hasCreateOrUpdateEffect` is true. So, we stop `||`-ing them, and instead give the new thing precedence. This way, for actions with hardcoded param sets like `user.signUp`, we never nest. This fixes the over-eager nesting issue for bulk actions as well.
1 parent 3ebf8e8 commit 5c7342f

File tree

3 files changed

+161
-122
lines changed

3 files changed

+161
-122
lines changed

packages/react/spec/useAction.spec.tsx

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -457,12 +457,10 @@ describe("useAction", () => {
457457
expect(promiseResult.error).toBeFalsy();
458458
});
459459

460-
expect(variables).toMatchInlineSnapshot(`
461-
{
462-
"email": "[email protected]",
463-
"password": "password123!",
464-
}
465-
`);
460+
expect(variables).toEqual({
461+
462+
password: "password123!",
463+
});
466464
});
467465

468466
test("should throw if called without a model api identifier and there is an ambiguous field", async () => {

packages/react/spec/useBulkAction.spec.ts

Lines changed: 152 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -102,31 +102,7 @@ describe("useBulkAction", () => {
102102
expect(result.current[0].error).toBeFalsy();
103103
});
104104

105-
test("can execute a bulk create with params", async () => {
106-
const mockBulkCreate = {
107-
type: "action",
108-
operationName: "bulkCreateWidgets",
109-
namespace: null,
110-
modelApiIdentifier: "widget",
111-
modelSelectionField: "widgets",
112-
isBulk: true,
113-
defaultSelection: {
114-
id: true,
115-
name: true,
116-
},
117-
selectionType: {},
118-
optionsType: {},
119-
schemaType: null,
120-
variablesType: void 0,
121-
variables: {
122-
inputs: {
123-
required: true,
124-
type: "[BulkCreateWidgetsInput!]",
125-
},
126-
},
127-
hasReturnType: false,
128-
} as any;
129-
105+
test("can execute a bulk create with flattened params", async () => {
130106
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
131107
// @ts-ignore waiting for bulk params to be released gadget side
132108
const { result } = renderHook(() => useBulkAction<any, any, any, any>(mockBulkCreate), { wrapper: MockClientWrapper(bulkExampleApi) });
@@ -141,18 +117,20 @@ describe("useBulkAction", () => {
141117
expect(result.current[0].error).toBeFalsy();
142118

143119
expect(mockUrqlClient.executeMutation).toBeCalledTimes(1);
144-
expect(mockUrqlClient.executeMutation.mock.calls[0][0].variables).toMatchInlineSnapshot(`
145-
{
146-
"inputs": [
147-
{
148-
"name": "foo",
120+
expect(mockUrqlClient.executeMutation.mock.calls[0][0].variables).toEqual({
121+
inputs: [
122+
{
123+
widget: {
124+
name: "foo",
149125
},
150-
{
151-
"name": "bar",
126+
},
127+
{
128+
widget: {
129+
name: "bar",
152130
},
153-
],
154-
}
155-
`);
131+
},
132+
],
133+
});
156134

157135
mockUrqlClient.executeMutation.pushResponse("bulkCreateWidgets", {
158136
data: {
@@ -183,30 +161,6 @@ describe("useBulkAction", () => {
183161
});
184162

185163
test("can execute a bulk create with fully qualified params", async () => {
186-
const mockBulkCreate = {
187-
type: "action",
188-
operationName: "bulkCreateWidgets",
189-
namespace: null,
190-
modelApiIdentifier: "widget",
191-
modelSelectionField: "widgets",
192-
isBulk: true,
193-
defaultSelection: {
194-
id: true,
195-
name: true,
196-
},
197-
selectionType: {},
198-
optionsType: {},
199-
schemaType: null,
200-
variablesType: void 0,
201-
variables: {
202-
inputs: {
203-
required: true,
204-
type: "[BulkCreateWidgetsInput!]",
205-
},
206-
},
207-
hasReturnType: false,
208-
} as any;
209-
210164
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
211165
// @ts-ignore waiting for bulk params to be released gadget side
212166
const { result } = renderHook(() => useBulkAction<any, any, any, any>(mockBulkCreate), { wrapper: MockClientWrapper(bulkExampleApi) });
@@ -221,22 +175,20 @@ describe("useBulkAction", () => {
221175
expect(result.current[0].error).toBeFalsy();
222176

223177
expect(mockUrqlClient.executeMutation).toBeCalledTimes(1);
224-
expect(mockUrqlClient.executeMutation.mock.calls[0][0].variables).toMatchInlineSnapshot(`
225-
{
226-
"inputs": [
227-
{
228-
"widget": {
229-
"name": "foo",
230-
},
178+
expect(mockUrqlClient.executeMutation.mock.calls[0][0].variables).toEqual({
179+
inputs: [
180+
{
181+
widget: {
182+
name: "foo",
231183
},
232-
{
233-
"widget": {
234-
"name": "bar",
235-
},
184+
},
185+
{
186+
widget: {
187+
name: "bar",
236188
},
237-
],
238-
}
239-
`);
189+
},
190+
],
191+
});
240192

241193
mockUrqlClient.executeMutation.pushResponse("bulkCreateWidgets", {
242194
data: {
@@ -266,40 +218,79 @@ describe("useBulkAction", () => {
266218
expect(result.current[0].error).toBeFalsy();
267219
});
268220

269-
test("can execute a bulk update with params", async () => {
270-
const mockBulkUpdate = {
271-
type: "action",
272-
operationName: "bulkUpdateWidgets",
273-
namespace: null,
274-
modelApiIdentifier: "widget",
275-
modelSelectionField: "widgets",
276-
isBulk: true,
277-
defaultSelection: {
278-
id: true,
279-
name: true,
280-
},
281-
selectionType: {},
282-
optionsType: {},
283-
schemaType: null,
284-
variablesType: void 0,
285-
variables: {
286-
inputs: {
287-
required: true,
288-
type: "[BulkUpdateWidgetsInput!]",
221+
test("can execute a bulk update with flattened params", async () => {
222+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
223+
// @ts-ignore waiting for bulk params to be released gadget side
224+
const { result } = renderHook(() => useBulkAction<any, any, any, any>(mockBulkUpdate), { wrapper: MockClientWrapper(bulkExampleApi) });
225+
226+
let mutationPromise: any;
227+
act(() => {
228+
mutationPromise = result.current[1]([
229+
{ id: "123", name: "foo" },
230+
{ id: "124", name: "bar" },
231+
]);
232+
});
233+
234+
expect(result.current[0].data).toBeFalsy();
235+
expect(result.current[0].fetching).toBe(true);
236+
expect(result.current[0].error).toBeFalsy();
237+
238+
expect(mockUrqlClient.executeMutation).toBeCalledTimes(1);
239+
expect(mockUrqlClient.executeMutation.mock.calls[0][0].variables).toEqual({
240+
inputs: [
241+
{
242+
id: "123",
243+
widget: {
244+
name: "foo",
245+
},
246+
},
247+
{
248+
id: "124",
249+
widget: {
250+
name: "bar",
251+
},
252+
},
253+
],
254+
});
255+
256+
mockUrqlClient.executeMutation.pushResponse("bulkUpdateWidgets", {
257+
data: {
258+
bulkUpdateWidgets: {
259+
success: true,
260+
widgets: [
261+
{ id: "123", name: "foo" },
262+
{ id: "124", name: "bar" },
263+
],
289264
},
290265
},
291-
hasReturnType: false,
292-
} as any;
266+
stale: false,
267+
hasNext: false,
268+
});
293269

270+
await act(async () => {
271+
const promiseResult = await mutationPromise;
272+
expect(promiseResult.data!.length).toEqual(2);
273+
expect(promiseResult.data![0].id).toEqual("123");
274+
expect(promiseResult.data![1].id).toEqual("124");
275+
});
276+
277+
expect(result.current[0].data!.length).toEqual(2);
278+
expect(result.current[0].data![0].id).toEqual("123");
279+
expect(result.current[0].data![1].id).toEqual("124");
280+
expect(result.current[0].fetching).toBe(false);
281+
expect(result.current[0].error).toBeFalsy();
282+
});
283+
284+
test("can execute a bulk update with fully qualified params", async () => {
294285
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
295286
// @ts-ignore waiting for bulk params to be released gadget side
296287
const { result } = renderHook(() => useBulkAction<any, any, any, any>(mockBulkUpdate), { wrapper: MockClientWrapper(bulkExampleApi) });
297288

298289
let mutationPromise: any;
299290
act(() => {
300291
mutationPromise = result.current[1]([
301-
{ id: "123", name: "foo" },
302-
{ id: "124", name: "bar" },
292+
{ id: "123", widget: { name: "foo" } },
293+
{ id: "124", widget: { name: "bar" } },
303294
]);
304295
});
305296

@@ -308,20 +299,22 @@ describe("useBulkAction", () => {
308299
expect(result.current[0].error).toBeFalsy();
309300

310301
expect(mockUrqlClient.executeMutation).toBeCalledTimes(1);
311-
expect(mockUrqlClient.executeMutation.mock.calls[0][0].variables).toMatchInlineSnapshot(`
312-
{
313-
"inputs": [
314-
{
315-
"id": "123",
316-
"name": "foo",
302+
expect(mockUrqlClient.executeMutation.mock.calls[0][0].variables).toEqual({
303+
inputs: [
304+
{
305+
id: "123",
306+
widget: {
307+
name: "foo",
317308
},
318-
{
319-
"id": "124",
320-
"name": "bar",
309+
},
310+
{
311+
id: "124",
312+
widget: {
313+
name: "bar",
321314
},
322-
],
323-
}
324-
`);
315+
},
316+
],
317+
});
325318

326319
mockUrqlClient.executeMutation.pushResponse("bulkUpdateWidgets", {
327320
data: {
@@ -440,3 +433,53 @@ describe("useBulkAction", () => {
440433
expect(result.current[0]).toBe(beforeObject);
441434
});
442435
});
436+
437+
const mockBulkCreate = {
438+
type: "action",
439+
operationName: "bulkCreateWidgets",
440+
namespace: null,
441+
modelApiIdentifier: "widget",
442+
modelSelectionField: "widgets",
443+
isBulk: true,
444+
defaultSelection: {
445+
id: true,
446+
name: true,
447+
},
448+
selectionType: {},
449+
optionsType: {},
450+
schemaType: null,
451+
variablesType: void 0,
452+
variables: {
453+
inputs: {
454+
required: true,
455+
type: "[BulkCreateWidgetsInput!]",
456+
},
457+
},
458+
acceptsModelInput: true,
459+
hasReturnType: false,
460+
} as any;
461+
462+
const mockBulkUpdate = {
463+
type: "action",
464+
operationName: "bulkUpdateWidgets",
465+
namespace: null,
466+
modelApiIdentifier: "widget",
467+
modelSelectionField: "widgets",
468+
isBulk: true,
469+
defaultSelection: {
470+
id: true,
471+
name: true,
472+
},
473+
selectionType: {},
474+
optionsType: {},
475+
schemaType: null,
476+
variablesType: void 0,
477+
variables: {
478+
inputs: {
479+
required: true,
480+
type: "[BulkUpdateWidgetsInput!]",
481+
},
482+
},
483+
acceptsModelInput: true,
484+
hasReturnType: false,
485+
} as any;

packages/react/src/utils.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -323,14 +323,12 @@ export const disambiguateActionVariables = (
323323
}
324324

325325
let newVariables: Record<string, any>;
326-
const idVariable = Object.entries(action.variables).find(([key, value]) => key === "id" && value.type === "GadgetID");
327326

328-
if (action.acceptsModelInput || action.hasCreateOrUpdateEffect) {
327+
if (action.acceptsModelInput ?? action.hasCreateOrUpdateEffect) {
329328
if (
330-
(action.modelApiIdentifier in variables &&
331-
typeof variables[action.modelApiIdentifier] === "object" &&
332-
variables[action.modelApiIdentifier] !== null) ||
333-
!action.variables[action.modelApiIdentifier]
329+
action.modelApiIdentifier in variables &&
330+
typeof variables[action.modelApiIdentifier] === "object" &&
331+
variables[action.modelApiIdentifier] != null
334332
) {
335333
newVariables = variables;
336334
} else {
@@ -341,7 +339,7 @@ export const disambiguateActionVariables = (
341339
if (action.paramOnlyVariables?.includes(key)) {
342340
newVariables[key] = value;
343341
} else {
344-
if (idVariable && key === idVariable[0]) {
342+
if (key == "id") {
345343
newVariables.id = value;
346344
} else {
347345
newVariables[action.modelApiIdentifier][key] = value;

0 commit comments

Comments
 (0)