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.
---------
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 ───────────────────────────────────────────────────────