diff --git a/.changeset/create-user-resultdialog-unwrap.md b/.changeset/create-user-resultdialog-unwrap.md new file mode 100644 index 000000000..9e06c0578 --- /dev/null +++ b/.changeset/create-user-resultdialog-unwrap.md @@ -0,0 +1,10 @@ +--- +"@object-ui/app-shell": patch +--- + +Fix "Create User" (and set_user_password / enable_two_factor / +create_oauth_application) result dialogs rendering an empty email + temporary +password: the console `apiHandler` now unwraps the `{ success, data }` response +envelope so `resultDialog` field paths resolve against the inner `data`, +matching `flowHandler` / `serverActionHandler` and the documented "path into +`data`" contract. Paired with framework#2842 (objectui#2396). diff --git a/packages/app-shell/src/hooks/__tests__/useConsoleActionRuntime.test.tsx b/packages/app-shell/src/hooks/__tests__/useConsoleActionRuntime.test.tsx index 1c28b8bbe..e39b6f193 100644 --- a/packages/app-shell/src/hooks/__tests__/useConsoleActionRuntime.test.tsx +++ b/packages/app-shell/src/hooks/__tests__/useConsoleActionRuntime.test.tsx @@ -90,6 +90,43 @@ describe('useConsoleActionRuntime — authenticated handlers', () => { expect(onRefresh).toHaveBeenCalledTimes(1); }); + it('apiHandler unwraps the `{ success, data }` envelope so result.data is the inner payload (create_user password reveal)', async () => { + // The admin/create-user wrapper returns `{ success, data: { user, + // temporaryPassword } }`. The action `resultDialog` field paths + // (`user.email`, `temporaryPassword`) are written relative to the INNER + // `data`, matching flowHandler / serverActionHandler which already unwrap. + // Pre-fix apiHandler leaked the whole envelope, so ActionResultDialog's + // readPath(envelope, 'user.email') resolved to undefined and BOTH the email + // and temporary-password fields rendered blank — the reported bug. + authFetchSpy.mockResolvedValue({ + ok: true, + json: async () => ({ + success: true, + data: { user: { id: 'u9', email: 'new@acme.co' }, temporaryPassword: 'Tmp-abc123!' }, + }), + }); + const { result } = renderHook(() => + useConsoleActionRuntime({ dataSource: {}, objects: [] }), + ); + + let res: any; + await act(async () => { + res = await result.current.apiHandler({ + type: 'api', name: 'create_user', target: '/api/v1/auth/admin/create-user', + params: { email: 'new@acme.co' }, + } as any); + }); + + expect(res.success).toBe(true); + // The inner payload — not the envelope. readPath(data, 'user.email') and + // readPath(data, 'temporaryPassword') now resolve in ActionResultDialog. + expect(res.data).toEqual({ + user: { id: 'u9', email: 'new@acme.co' }, + temporaryPassword: 'Tmp-abc123!', + }); + expect((res.data as any).success).toBeUndefined(); + }); + it('apiHandler surfaces a failed response and does not refresh', async () => { authFetchSpy.mockResolvedValue({ ok: false, status: 403, json: async () => ({ error: 'Forbidden' }) }); const onRefresh = vi.fn(); diff --git a/packages/app-shell/src/hooks/useConsoleActionRuntime.tsx b/packages/app-shell/src/hooks/useConsoleActionRuntime.tsx index 3b6344d75..eb54f9ab6 100644 --- a/packages/app-shell/src/hooks/useConsoleActionRuntime.tsx +++ b/packages/app-shell/src/hooks/useConsoleActionRuntime.tsx @@ -315,8 +315,22 @@ export function useConsoleActionRuntime(opts: ConsoleActionRuntimeOptions): Cons const detail = (body as any)?.error || (body as any)?.message || `HTTP ${res.status}`; return { success: false, error: detail }; } - const data = await res.json().catch(() => ({})); + const json = await res.json().catch(() => ({})); if (action.refreshAfter !== false) refresh(); + // Unwrap the ObjectStack `{ success, data }` envelope so `result.data` + // is the inner payload — the contract every `result.data` consumer + // expects. The action `resultDialog` field paths (e.g. `user.email`, + // `temporaryPassword`) and the dynamic-toast `result.data.message` are + // all written relative to the inner `data`. flowHandler and + // serverActionHandler already unwrap `json.data`; apiHandler was the + // lone handler that leaked the whole envelope, which blanked every + // resultDialog whose paths didn't redundantly prefix `data.` (the + // "Create User temporary password shows empty" bug). Bare, + // non-enveloped responses (some stock better-auth bodies) pass through + // unchanged. + const data = json && typeof json === 'object' && !Array.isArray(json) && 'data' in json + ? (json as { data: unknown }).data + : json; return { success: true, data, reload: action.refreshAfter !== false }; }