From 4a0f2b177865fb3138c71a3be8c060f2bb1292ac Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2026 09:31:06 -0700 Subject: [PATCH] Default user export to filtered scope; note Anonymous in all-users label (#1679) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Two changes to the user data export dialog on the project Users page: 1. The export scope now defaults to **"Export only filtered/searched users"** instead of "all users". 2. The all-users option label is now **"Export all users in the project (includes Anonymous)"**. To keep this scoped to the Users table (the shared export dialog is reused by other tables), the dialog's default scope is made configurable rather than changed globally: - `DataGridExportOptions` gains `defaultScope?: DataGridExportScope` (defaults to `"all"`). - The dialog initializes `useState(exportOptions?.defaultScope ?? "all")`. - `user-table.tsx` passes `defaultScope: "filtered"` and the updated `allScopeLabel`. Other tables (teams, transactions, emails) are unaffected — they keep the `"all"` default. Link to Devin session: https://app.devin.ai/sessions/4996678b2b944090b6eef2f64f0a62a1 --- ## Summary by cubic Default the Users export dialog to filtered scope and clarify that the "all users" option includes Anonymous; scope resets to the per-table default only when the dialog reopens, and other tables keep "all". - **New Features** - Added `defaultScope` to export options and initialized scope from it; Users table sets `defaultScope: "filtered"` and updates the all-users label. - Reset scope to `defaultScope` only on a closed→open transition to avoid changing it while the dialog is open. - **Bug Fixes** - Stubbed `NODE_ENV` via `vi.stubEnv` in `apps/backend/src/oauth/ssrf-protection.test.ts` to fix lint and prevent env mutation. Written for commit 3aa670b6d2249af737d4d05506b2f3a3737b075c. Summary will update on new commits. Review in cubic --------- Co-authored-by: vedanta.gawande Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../backend/src/oauth/ssrf-protection.test.ts | 11 +++------- .../src/components/data-table/user-table.tsx | 3 ++- .../data-grid/data-grid-export-dialog.tsx | 20 +++++++++++++++++-- .../src/components/data-grid/types.ts | 2 ++ 4 files changed, 25 insertions(+), 11 deletions(-) diff --git a/apps/backend/src/oauth/ssrf-protection.test.ts b/apps/backend/src/oauth/ssrf-protection.test.ts index 330fb7f25..68b001db3 100644 --- a/apps/backend/src/oauth/ssrf-protection.test.ts +++ b/apps/backend/src/oauth/ssrf-protection.test.ts @@ -1,19 +1,14 @@ import { StatusError } from "@hexclave/shared/dist/utils/errors"; -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import dns from "node:dns"; import { assertSafeOAuthResolvedAddress, assertSafeOAuthUrlWithoutDns, isBlockedOAuthIpAddress, safeOAuthDnsLookup } from "./ssrf-protection"; async function withProductionNodeEnv(callback: () => Promise): Promise { - const previousNodeEnv = process.env.NODE_ENV; - process.env.NODE_ENV = "production"; + vi.stubEnv("NODE_ENV", "production"); try { return await callback(); } finally { - if (previousNodeEnv === undefined) { - delete process.env.NODE_ENV; - } else { - process.env.NODE_ENV = previousNodeEnv; - } + vi.unstubAllEnvs(); } } diff --git a/apps/dashboard/src/components/data-table/user-table.tsx b/apps/dashboard/src/components/data-table/user-table.tsx index 9d726663e..f0f44411c 100644 --- a/apps/dashboard/src/components/data-table/user-table.tsx +++ b/apps/dashboard/src/components/data-table/user-table.tsx @@ -426,7 +426,8 @@ function UserTableBody(props: { fetchRows: fetchExportRows, emptyExportTitle: "No users to export", emptyExportDescription: "There are no users matching the current filters", - allScopeLabel: "Export all users in the project", + defaultScope: "filtered", + allScopeLabel: "Export all users in the project (includes Anonymous)", filteredScopeLabel: ( <> Export only filtered/searched users diff --git a/packages/dashboard-ui-components/src/components/data-grid/data-grid-export-dialog.tsx b/packages/dashboard-ui-components/src/components/data-grid/data-grid-export-dialog.tsx index 4c4e3d722..249b158a7 100644 --- a/packages/dashboard-ui-components/src/components/data-grid/data-grid-export-dialog.tsx +++ b/packages/dashboard-ui-components/src/components/data-grid/data-grid-export-dialog.tsx @@ -1,7 +1,7 @@ "use client"; import { DownloadSimpleIcon } from "@phosphor-icons/react"; -import React, { useCallback, useEffect, useMemo, useState } from "react"; +import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { DesignButton } from "../button"; import { DesignDialog } from "../dialog"; @@ -55,7 +55,7 @@ export function DataGridExportDialog({ [exportOptions?.fields, columns], ); const [format, setFormat] = useState("csv"); - const [scope, setScope] = useState("all"); + const [scope, setScope] = useState(exportOptions?.defaultScope ?? "all"); const [fields, setFields] = useState[]>(resolvedFields); const [isExporting, setIsExporting] = useState(false); const [progress, setProgress] = useState(idleExportProgress); @@ -67,6 +67,22 @@ export function DataGridExportDialog({ } }, [isExporting, resolvedFields]); + // Reset the scope to its default each time the dialog opens. The dialog stays + // mounted between opens, so without this the scope would retain whatever the + // user last picked instead of honoring `defaultScope` on every open. We track + // the previous `open` value with a ref so the reset only fires on a genuine + // closed->open transition -- not on every render that flips other state (e.g. + // `isExporting` going false after a failed/empty export would otherwise wipe + // the user's current selection while the dialog is still open). + const defaultScope = exportOptions?.defaultScope ?? "all"; + const wasOpenRef = useRef(false); + useEffect(() => { + if (open && !wasOpenRef.current) { + setScope(defaultScope); + } + wasOpenRef.current = open; + }, [open, defaultScope]); + const entityName = exportOptions?.entityName ?? "row"; const entityNamePlural = exportOptions?.entityNamePlural ?? "rows"; const filenamePrefix = exportOptions?.filenamePrefix ?? exportFilename; diff --git a/packages/dashboard-ui-components/src/components/data-grid/types.ts b/packages/dashboard-ui-components/src/components/data-grid/types.ts index 965e90894..f4b6ea286 100644 --- a/packages/dashboard-ui-components/src/components/data-grid/types.ts +++ b/packages/dashboard-ui-components/src/components/data-grid/types.ts @@ -251,6 +251,8 @@ export type DataGridExportOptions = { allScopeLabel?: ReactNode; filteredScopeLabel?: ReactNode; progressSubjectLabel?: string; + /** Which export scope is selected by default when the dialog opens. Defaults to `"all"`. */ + defaultScope?: DataGridExportScope; }; // ─── Callbacks ───────────────────────────────────────────────────────