Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion apps/desktop/src/lib/trpc/routers/external/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
} from "@superset/local-db";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { clipboard, shell } from "electron";
import { app, clipboard, shell } from "electron";
import { localDb } from "main/lib/local-db";
import { externalUrlLogLabel, isSafeExternalUrl } from "main/lib/safe-url";
import { z } from "zod";
Expand Down Expand Up @@ -146,6 +146,29 @@ export const createExternalRouter = () => {
shell.showItemInFolder(input);
}),

saveToDownloads: publicProcedure
.input(
z.object({
filename: z.string().min(1),
// ~10MB attachment cap → ~14M base64 chars; reject anything wilder.
dataBase64: z.string().max(20_000_000),
}),
)
.mutation(async ({ input }) => {
const safeName = nodePath.basename(input.filename) || "download";
const downloadsDir = app.getPath("downloads");
const { name, ext } = nodePath.parse(safeName);
let target = nodePath.join(downloadsDir, safeName);
for (let i = 1; fs.existsSync(target); i++) {
target = nodePath.join(downloadsDir, `${name} (${i})${ext}`);
}
await fs.promises.writeFile(
target,
Buffer.from(input.dataBase64, "base64"),
);
Comment on lines +162 to +168

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Locate file and inspect relevant sections"
fd -a 'index.ts' . | sed 's#^\./##' | rg 'apps/desktop/src/lib/trpc/routers/external/index.ts' || true

if [ -f apps/desktop/src/lib/trpc/routers/external/index.ts ]; then
  wc -l apps/desktop/src/lib/trpc/routers/external/index.ts
  sed -n '120,185p' apps/desktop/src/lib/trpc/routers/external/index.ts | cat -n
  echo
  echo "Search for procedure definitions/imports"
  sed -n '1,120p' apps/desktop/src/lib/trpc/routers/external/index.ts | cat -n
fi

echo
echo "Search for callers/similar filename handling"
rg -n "input\.dataBase64|downloadsDir|base64|externalRouter|\.download|dataBase64" apps/desktop/src/lib/trpc apps/desktop/src -g '*.{ts,tsx}' || true

echo
echo "Node TOCTOU probe if node is available"
node - <<'JS'
const fs = require('fs');
const os = require('os');
const path = require('path');
const child = require('worker_threads').parentPort;
const shared = Buffer.alloc(4);
const { SharedArrayBuffer, Atomics } = require('worker_threads');
if (!child || !(shared instanceof SharedArrayBuffer)) {
  console.log('node worker/shared-memory probe unavailable in this execution context');
  process.exit(0);
}
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'node-tocouto-'));
const file = path.join(dir, 'same.txt');
(async () => {
  await Promise.all([
    new Promise(async (resolve) => {
      const w = child.constructor;
      const worker = new w(`
        const { Atomics, SharedArrayBuffer, workerData } = require('worker_threads');
        const shared = new SharedArrayBuffer(4);
        const sharedView = new Int32Array(shared);
        const file = workerData[0];
        Atomics.store(sharedView, 0, 0);
        Atomics.store(sharedView, 1, 1);
        require('fs').existsSync(file);
        Atomics.wait(sharedView, 0, 1);
        const first = require('fs').existsSync(file);
        try {
          return require('fs').openSync(file, 'wx', 0o644);
        } catch (error) {
          return error && error.code;
        }
      `, { workerData: [file] });
      worker.on('error', (err) => console.log('worker error', err.message));
      worker.on('exit', () => resolve(Atomics.load(new Int32Array(shared), 1)));
      Atomics.wait(new Int32Array(shared), 1, 1);
      Atomics.wait(new Int32Array(shared), 0, 0);
    }),
    new Promise((resolve) => {
      const w = child.constructor;
      const worker = new w(`
        const { Atomics, SharedArrayBuffer } = require('worker_threads');
        const shared = new Int32Array(workerData[0]);
        Atomics.store(shared, 0, 1);
        const ready = Atomics.wait(shared, 0, 0);
        return ready;
      `, { workerData: Int32Array.buffer });
      worker.on('error', (err) => console.log('worker error', err.message));
      worker.on('exit', () => resolve());
    }),
  ]);
  fs.writeFileSync(file, 'lost');
  await child.postMessage({ ok: 'done' });
})();
JS

Repository: superset-sh/superset

Length of output: 24656


🌐 Web query:

Node.js fs.writeFile exclusive flag EEXIST race condition

💡 Result:

Yes, using the wx flag with fs.writeFile (or the corresponding file system method) is the correct, recommended way to prevent race conditions when creating files [1][2]. The 'wx' flag stands for "write" and "exclusive" (x) [3]. When this flag is used, the underlying operating system performs the check for the file's existence and the file creation as a single, atomic operation [1]. This atomicity is critical because it eliminates the "time-of-check to time-of-use" (TOCTOU) race condition that occurs if you attempt to manually check for a file's existence using methods like fs.exists (deprecated) or fs.stat before writing [4][1][5]. In those manual approaches, another process could potentially create the file in the small window of time between your check and your write command, leading to an accidental overwrite [1]. When you use the wx flag: 1. The operation will fail if the file already exists, and the error returned will have the code EEXIST [1][5]. 2. If the file does not exist, it will be created and written to atomically [1]. Important considerations: - Handle the error: Since 'wx' causes an error when the file exists, your code must explicitly catch and handle the EEXIST error code to respond appropriately (e.g., by logging a message or choosing a different filename) [1][5]. - Not a general lock: While the wx flag ensures atomic creation, it does not act as a general file lock for subsequent read/write operations [3]. If you need to perform multiple operations on the same file without other processes interfering, you should look into more robust file locking mechanisms or architectural patterns [3]. - Don't use existence checks first: Official Node.js documentation explicitly warns against using fs.access or similar checks to test for file accessibility before calling file system methods, as this introduces the very race condition that the flags are designed to avoid [4][5].

Citations:


Make collision handling atomic.

The existsSync(target) probe and subsequent writeFile are separate operations, so concurrent saves resolving to the same filename can race and one can be lost. Use an exclusive write (wx) loop that retries on EEXIST instead of probing before each write.

🧰 Tools
🪛 ast-grep (0.44.1)

[warning] 164-167: Filesystem path is not a string literal; a request-/variable-derived path can enable path traversal. Validate and normalize the path before use.
Context: fs.promises.writeFile(
target,
Buffer.from(input.dataBase64, "base64"),
)
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(detect-non-literal-fs-filename-typescript)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/lib/trpc/routers/external/index.ts` around lines 162 - 168,
Update the download save logic around the target filename loop to use exclusive
file creation with the writeFile flag "wx" instead of existsSync probing. Retry
with the next suffixed filename only when the write fails with EEXIST, and
preserve the existing base name, extension, directory, and base64 content
behavior.

Source: Linters/SAST tools

return { path: target };
}),

openInApp: publicProcedure
.input(
z.object({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,11 @@ interface MarkdownEditorProps {
className?: string;
editorClassName?: string;
onModEnter?: () => void;
/**
* If provided, plain Enter fires this instead of inserting a newline
* (Shift+Enter still breaks the line). Composer-style editors only.
*/
onEnterSubmit?: () => void;
/** If provided, enables @-mention file search for the editor. */
searchFiles?: FileMentionSearchFn;
/** If provided, pasted file items (e.g. clipboard images) are forwarded here. */
Expand Down Expand Up @@ -203,6 +208,7 @@ export function MarkdownEditor({
className,
editorClassName,
onModEnter,
onEnterSubmit,
searchFiles,
onPasteFiles,
features,
Expand All @@ -218,6 +224,8 @@ export function MarkdownEditor({
searchFilesRef.current = searchFiles;
const onPasteFilesRef = useRef(onPasteFiles);
onPasteFilesRef.current = onPasteFiles;
const onEnterSubmitRef = useRef(onEnterSubmit);
onEnterSubmitRef.current = onEnterSubmit;
const editorRef = useRef<Editor | null>(null);

const urlPolicy = useInlineUrlPolicy();
Expand Down Expand Up @@ -353,6 +361,15 @@ export function MarkdownEditor({
onModEnter?.();
return true;
}
if (
onEnterSubmitRef.current &&
event.key === "Enter" &&
!event.shiftKey &&
!event.altKey
) {
onEnterSubmitRef.current();
return true;
}
Comment on lines +364 to +372

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does ProseMirror handleKeyDown fire during IME composition isComposing keyCode 229

💡 Result:

ProseMirror is designed to ignore key events during IME composition to prevent them from interfering with the browser's native handling of composition sessions [1][2]. In its core implementation, ProseMirror's keydown handler utilizes an internal function—typically inOrNearComposition(view, event)—to check if a composition is currently active or recently ended [1][2]. When this check identifies that an IME composition is in progress, the handler returns early, effectively preventing the handleKeyDown prop from being invoked [1][2]. Regarding keyCode 229: 1. The value 229 is the "Composition" key code, a legacy identifier often used by browsers (notably older versions of Safari) to signal that a key event is being processed as part of an IME composition session [3][4][5]. 2. While modern browsers primarily use the isComposing flag on the event object to indicate composition, ProseMirror acknowledges the complexity of cross-browser IME behavior [2][5]. 3. ProseMirror's internal logic avoids triggering standard keydown processing when isComposing is true or when it detects that the event is associated with an active composition state [1][2]. If you are writing custom plugins or handlers, you should similarly guard your handleKeyDown logic by checking event.isComposing [5]. Additionally, some developers include a check for event.keyCode === 229 as a fallback to ensure compatibility with older or specific browser environments that may not reliably set the isComposing flag [4][5][6]. Top results: [1][2][5]

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Locate MarkdownEditor file and related package files"
fd -a 'MarkdownEditor\.tsx|package\.json|pnpm-lock\.yaml|yarn\.lock|package-lock\.json' . | sed 's#^\./##' | head -80

echo
echo "MarkdownEditor outline"
ast-grep outline apps/desktop/src/renderer/components/MarkdownEditor/MarkdownEditor.tsx --view expanded || true

echo
echo "Relevant MarkdownEditor sections"
wc -l apps/desktop/src/renderer/components/MarkdownEditor/MarkdownEditor.tsx
sed -n '200,390p' apps/desktop/src/renderer/components/MarkdownEditor/MarkdownEditor.tsx

echo
echo "Search for tiptap/prosemirror imports and usages"
rg -n "tiptap|prosemirror|handleKeyDown|onEnterSubmit|isComposing|ProseMirror" apps/desktop/src/renderer/components/MarkdownEditor apps/desktop -g '*.{ts,tsx}' -g 'package.json' -g 'pnpm-lock.yaml' -g 'yarn.lock' -g 'package-lock.json' | head -200

Repository: superset-sh/superset

Length of output: 40431


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Lockfile/package manager"
git ls-files | rg '(^|/)package\.json$|(^|/)pnpm-lock\.yaml$|(^|/)yarn\.lock$|(^|/)package-lock\.json$' | head -20

echo
echo "Desktop package dependencies"
node - <<'JS'
const fs = require('fs');
const p = JSON.parse(fs.readFileSync('apps/desktop/package.json','utf8'));
for (const section of ['dependencies','devDependencies']) {
  console.log(`\n[${section}]`);
  for (const [k,v] of Object.entries(p[section]||{}).filter(([k]) => k.includes('tiptap') || k.includes('prosemirror') || k.includes('electron'))) {
    console.log(`${k}: ${v}`);
  }
}
JS

echo
echo "Root lock snippets for tiptap/prosemirror/electron"
for f in pnpm-lock.yaml yarn.lock package-lock.json; do
  [ -f "$f" ] || continue
  echo "--- $f ---"
  rg -n "`@tiptap`|prosemirror|electron(-main|preload)?|electron(\@|:)|desktop app|electron|`@electron`" "$f" | head -120 || true
done

echo
echo "KeyboardHandler implementation"
sed -n '100,135p' apps/desktop/src/renderer/components/MarkdownEditor/MarkdownEditor.tsx

Repository: superset-sh/superset

Length of output: 3265


🌐 Web query:

@tiptap/react 3 handleKeyDown isComposing composition events ProseMirror

💡 Result:

In Tiptap (built on ProseMirror), handling keyboard events during an IME composition session requires caution because browsers fire events in complex sequences that can cause input corruption if intercepted improperly [1][2]. To properly manage handleKeyDown in a Tiptap or ProseMirror context while respecting composition: 1. Use event.isComposing: The standard way to detect if a keyboard event is occurring during an IME composition session is to check the read-only isComposing property of the KeyboardEvent [3]. In your handleKeyDown plugin prop, you should generally ignore events where event.isComposing is true to avoid interfering with the native composition process [2]. 2. Understand ProseMirror's Role: ProseMirror's core view logic already includes internal guards—such as inOrNearComposition—to prevent the editor from attempting to handle key events that occur during or immediately around an IME composition session [1]. Interfering with this by trying to force-handle or block events can lead to duplicated characters, incorrect text placement, or broken composition flow [2]. 3. Implementation Example: When writing a custom extension that uses a ProseMirror plugin to intercept keys, you can implement the check as follows: import { Extension } from '@tiptap/core'; import { Plugin, PluginKey } from 'prosemirror-state'; export const MyKeyboardExtension = Extension.create({ name: 'my-keyboard-extension', addProseMirrorPlugins { return [ new Plugin({ key: new PluginKey('my-key-handler'), props: { handleKeyDown: (view, event) => { // Ignore events during composition to prevent input corruption if (event.isComposing) { return false; } // Your custom key handling logic if (event.key === 'Enter') { //... your logic... return true; } return false; }, }, }), ]; }, }); Key Considerations: - Do not attempt to start or end composition events programmatically; these are managed by the browser's IME [4][5]. - Be aware that on certain mobile browsers (particularly Android/Chrome), composition events can behave unpredictably [6][1]. The native ProseMirror input handlers are specifically designed to suppress certain keys (like Enter) during these states to maintain stability [1]. - If you find your logic is still being triggered during composition, ensure your check is placed at the very beginning of your handleKeyDown handler [2].

Citations:


🌐 Web query:

Tiptap 3 handleKeyDown composition events source code event.isComposing

💡 Result:

In Tiptap (which is built on ProseMirror), composition events and the isComposing state are handled primarily at the ProseMirror level rather than within Tiptap's core source code [1][2]. When you implement custom keyboard handling via Tiptap extensions—such as using the handleKeyDown prop—it is important to respect the browser's native composition session to avoid issues with IME (Input Method Editor) input, which is common for languages like Chinese, Japanese, and Korean [1][2]. The KeyboardEvent.isComposing property is a standard Web API that indicates whether a key event occurs during a composition session (i.e., after compositionstart and before compositionend) [3]. In ProseMirror's event handling architecture, this is used to prevent the editor from prematurely processing key inputs that are intended to be part of an ongoing composition [4][2]. If you are using handleKeyDown in a Tiptap extension, you can check the isComposing property on the event object to decide whether to bypass your custom logic [5][2]. A common pattern looks like this: handleKeyDown(view, event) { if (event.isComposing) { return false; // Let the browser/ProseMirror handle the event during composition } // Your custom logic here return true; } ProseMirror internally manages composition events (via compositionstart, compositionupdate, and compositionend) to ensure the document state remains consistent with the DOM [4][2]. Manually interfering with these events is generally discouraged, as the browser must handle them normally for composition to function correctly [2]. Issues involving composition in Tiptap are often inherited from ProseMirror's input handling, where special care is required to ensure that editor state updates do not interrupt or prematurely finalize a composition session [1][4].

Citations:


Check event.isComposing before submitting on Enter.

Even though Ti/Tiptap uses ProseMirror under the hood, this custom editorProps.handleKeyDown can still observe key events during IME composition. Guarding with !event.isComposing prevents committing an IME candidate from firing onEnterSubmit and submitting prematurely.

🐛 Proposed guard
 				if (
 					onEnterSubmitRef.current &&
 					event.key === "Enter" &&
+					!event.isComposing &&
 					!event.shiftKey &&
 					!event.altKey
 				) {
 					onEnterSubmitRef.current();
 					return true;
 				}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (
onEnterSubmitRef.current &&
event.key === "Enter" &&
!event.shiftKey &&
!event.altKey
) {
onEnterSubmitRef.current();
return true;
}
if (
onEnterSubmitRef.current &&
event.key === "Enter" &&
!event.isComposing &&
!event.shiftKey &&
!event.altKey
) {
onEnterSubmitRef.current();
return true;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/renderer/components/MarkdownEditor/MarkdownEditor.tsx`
around lines 364 - 372, Update the Enter submission condition in the custom
editor keydown handler to also require !event.isComposing before invoking
onEnterSubmitRef.current(), preserving submission for non-composition Enter
presses.

return false;
},
handlePaste: (_, event) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ function DashboardLayout() {
v2WorkspaceMatch !== false ? v2WorkspaceMatch.workspaceId : null;
const onV1WorkspaceRoute = currentWorkspaceMatch !== false;
const onV2WorkspaceRoute = v2WorkspaceMatch !== false;
const onNewWorkspaceRoute = matchRoute({ to: "/new-workspace" }) !== false;
const versionMismatch =
(isV2CloudEnabled && onV1WorkspaceRoute) ||
(!isV2CloudEnabled && onV2WorkspaceRoute);
Expand Down Expand Up @@ -185,10 +186,12 @@ function DashboardLayout() {

// On the v2 workspace route with an expanded sidebar the TopBar row is
// merged into the pane tab bar (which provides the drag region and hosts
// the right-sidebar toggle). Collapsed/closed sidebars keep the TopBar:
// its inset is what keeps content clear of the macOS traffic lights.
// the right-sidebar toggle). The new-workspace page brings its own drag
// strip. Collapsed/closed sidebars keep the TopBar: its inset is what
// keeps content clear of the macOS traffic lights.
const hideTopBar =
onV2WorkspaceRoute && !versionMismatch && sidebarOutsideColumn;
((onV2WorkspaceRoute && !versionMismatch) || onNewWorkspaceRoute) &&
sidebarOutsideColumn;

return (
<div className="flex h-full w-full overflow-hidden">
Expand All @@ -198,7 +201,7 @@ function DashboardLayout() {
{!hideTopBar && <TopBar />}
<div className="flex flex-1 min-h-0 min-w-0 overflow-hidden">
{!sidebarOutsideColumn && sidebarPanel}
<div className="flex flex-1 min-h-0 min-w-0">
<div className="relative flex flex-1 min-h-0 min-w-0">
{versionMismatch ? <CrossVersionMismatchState /> : <Outlet />}
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { PromptInputProvider } from "@superset/ui/ai-elements/prompt-input";
import { createFileRoute } from "@tanstack/react-router";
import { NewWorkspaceScreen } from "renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/NewWorkspaceScreen";
import { DashboardNewWorkspaceDraftProvider } from "renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/DashboardNewWorkspaceDraftContext";

export const Route = createFileRoute(
"/_authenticated/_dashboard/new-workspace/",
)({
validateSearch: (
search: Record<string, unknown>,
): { projectId?: string } => ({
projectId:
typeof search.projectId === "string" ? search.projectId : undefined,
}),
component: NewWorkspacePage,
});

/**
* Experiment test arm (new-workspace-screen): the create surface as a real
* route. Store opens are redirected here by DashboardNewWorkspaceModal.
*/
function NewWorkspacePage() {
const { projectId } = Route.useSearch();
return (
<DashboardNewWorkspaceDraftProvider onClose={() => {}}>
<PromptInputProvider>
<NewWorkspaceScreen isOpen preSelectedProjectId={projectId ?? null} />
{/* Window-drag surface replacing the hidden TopBar's drag region. */}
<div className="drag absolute inset-x-0 top-0 z-50 h-12" />
</PromptInputProvider>
</DashboardNewWorkspaceDraftProvider>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { PromptInputProvider } from "@superset/ui/ai-elements/prompt-input";
import { NewWorkspaceScreen } from "renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/NewWorkspaceScreen";
import { DashboardNewWorkspaceDraftProvider } from "renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/DashboardNewWorkspaceDraftContext";

/**
* Experiment test arm (new-workspace-screen): replaces the "No workspaces yet"
* empty state with the create screen itself. With zero workspaces there is
* nothing behind it, so it is pinned — close actions (Escape) are no-ops.
* Store opens are not handled here: DashboardNewWorkspaceModal redirects them
* to the /new-workspace route.
*/
export function NewWorkspaceEmptyScreen() {
return (
<DashboardNewWorkspaceDraftProvider onClose={() => {}}>
<PromptInputProvider>
<NewWorkspaceScreen isOpen preSelectedProjectId={null} />
</PromptInputProvider>
</DashboardNewWorkspaceDraftProvider>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { NewWorkspaceEmptyScreen } from "./NewWorkspaceEmptyScreen";
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import { createFileRoute } from "@tanstack/react-router";
import { useEffect } from "react";
import { useNewWorkspaceScreenVariant } from "renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/hooks/useNewWorkspaceScreenVariant";
import { NewWorkspaceEmptyScreen } from "./components/NewWorkspaceEmptyScreen";
import { V2WorkspacesHeader } from "./components/V2WorkspacesHeader";
import { V2WorkspacesList } from "./components/V2WorkspacesList";
import { useAccessibleV2Workspaces } from "./hooks/useAccessibleV2Workspaces";
import { useV2WorkspacesFilterStore } from "./stores/v2WorkspacesFilterStore";
import {
DEVICE_FILTER_THIS_DEVICE,
PROJECT_FILTER_ALL,
useV2WorkspacesFilterStore,
} from "./stores/v2WorkspacesFilterStore";

export const Route = createFileRoute(
"/_authenticated/_dashboard/v2-workspaces/",
Expand Down Expand Up @@ -34,6 +40,22 @@ function V2WorkspacesPage() {
projectFilter,
});

// Experiment test arm: with zero workspaces the dashboard IS the create
// screen — the "No workspaces yet" empty state never shows. Gated on
// isReady (never pin over rows that are still settling) and on default
// filters so a filtered-to-empty view keeps the normal empty state.
// Evaluating the flag here is the exposure moment.
const isEmptyDashboard =
isReady &&
all.length === 0 &&
!searchQuery.trim() &&
projectFilter === PROJECT_FILTER_ALL &&
deviceFilter === DEVICE_FILTER_THIS_DEVICE;
const variant = useNewWorkspaceScreenVariant(isEmptyDashboard);
if (variant === "test" && isEmptyDashboard) {
return <NewWorkspaceEmptyScreen />;
}

return (
<div className="flex h-full w-full flex-1 flex-col overflow-hidden">
<V2WorkspacesHeader
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
DialogHeader,
DialogTitle,
} from "@superset/ui/dialog";
import { useNavigate } from "@tanstack/react-router";
import { useEffect, useRef } from "react";
import {
useCloseNewWorkspaceModal,
Expand All @@ -20,6 +21,7 @@ import {
DashboardNewWorkspaceDraftProvider,
useDashboardNewWorkspaceDraft,
} from "./DashboardNewWorkspaceDraftContext";
import { useNewWorkspaceScreenVariant } from "./hooks/useNewWorkspaceScreenVariant";

/** Clears the PromptInputProvider text & attachments when the draft resets. */
function PromptInputResetSync() {
Expand All @@ -42,6 +44,25 @@ export function DashboardNewWorkspaceModal() {
const isOpen = useNewWorkspaceModalOpen();
const closeModal = useCloseNewWorkspaceModal();
const preSelectedProjectId = usePreSelectedProjectId();
const navigate = useNavigate();
const variant = useNewWorkspaceScreenVariant(isOpen);
const isScreen = variant === "test";

// Test arm: the create surface is a page, not a modal. Store opens (the
// "+" button, hotkey, onboarding hand-off) redirect to the route instead.
useEffect(() => {
if (!isScreen || !isOpen) return;
closeModal();
void navigate({
to: "/new-workspace",
search: preSelectedProjectId
? { projectId: preSelectedProjectId }
: undefined,
});
}, [isScreen, isOpen, closeModal, navigate, preSelectedProjectId]);

if (isOpen && variant === null) return null;
if (isScreen) return null;

return (
<DashboardNewWorkspaceDraftProvider onClose={closeModal}>
Expand Down
Loading
Loading