diff --git a/apps/desktop/docs/EXTERNAL_FILES.md b/apps/desktop/docs/EXTERNAL_FILES.md index abcc47f5bb8..5cc01ad97cb 100644 --- a/apps/desktop/docs/EXTERNAL_FILES.md +++ b/apps/desktop/docs/EXTERNAL_FILES.md @@ -42,7 +42,7 @@ its hook entries into these files while preserving user-defined entries: | File | Purpose | |------|---------| | `~/.claude/settings.json` | Claude Code hook registration merge | -| `~/.codex/hooks.json` | Codex hook registration merge (`SessionStart`, `UserPromptSubmit`, `PreToolUse`, `PostToolUse`, `Stop`) | +| `~/.codex/hooks.json` | Codex hook registration merge (`SessionStart`, `UserPromptSubmit`, `Stop`) | | `~/.factory/settings.json` | Factory Droid hook registration (`UserPromptSubmit`, `Notification`, `PostToolUse`, `Stop`) | For Codex specifically, Superset now relies on native `~/.codex/hooks.json` diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 88be9c665bc..881ff16dcdd 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -2,7 +2,7 @@ "name": "@superset/desktop", "productName": "Superset", "description": "The last developer tool you'll ever need", - "version": "1.4.6", + "version": "1.4.7", "main": "./dist/main/index.js", "resources": "src/resources", "repository": { diff --git a/apps/desktop/src/main/lib/agent-setup/agent-wrappers-claude-codex-opencode.ts b/apps/desktop/src/main/lib/agent-setup/agent-wrappers-claude-codex-opencode.ts index e3378f804f9..db889900cd5 100644 --- a/apps/desktop/src/main/lib/agent-setup/agent-wrappers-claude-codex-opencode.ts +++ b/apps/desktop/src/main/lib/agent-setup/agent-wrappers-claude-codex-opencode.ts @@ -385,12 +385,7 @@ export function getCodexGlobalHooksJsonContent( } const managedEvents: Array<{ - eventName: - | "SessionStart" - | "UserPromptSubmit" - | "PreToolUse" - | "PostToolUse" - | "Stop"; + eventName: "SessionStart" | "UserPromptSubmit" | "Stop"; definition: ClaudeHookDefinition; }> = [ { @@ -405,20 +400,6 @@ export function getCodexGlobalHooksJsonContent( hooks: [{ type: "command", command: notifyScriptPath }], }, }, - { - eventName: "PreToolUse", - definition: { - matcher: "*", - hooks: [{ type: "command", command: notifyScriptPath }], - }, - }, - { - eventName: "PostToolUse", - definition: { - matcher: "*", - hooks: [{ type: "command", command: notifyScriptPath }], - }, - }, { eventName: "Stop", definition: { diff --git a/apps/desktop/src/main/lib/agent-setup/agent-wrappers.test.ts b/apps/desktop/src/main/lib/agent-setup/agent-wrappers.test.ts index 9c968c4c7b6..9e96a3e4912 100644 --- a/apps/desktop/src/main/lib/agent-setup/agent-wrappers.test.ts +++ b/apps/desktop/src/main/lib/agent-setup/agent-wrappers.test.ts @@ -888,7 +888,7 @@ describe("agent-wrappers codex hooks.json", () => { rmSync(TEST_ROOT, { recursive: true, force: true }); }); - it("creates Codex hooks.json with prompt and tool lifecycle hooks when no file exists", () => { + it("creates Codex hooks.json with prompt and lifecycle hooks when no file exists", () => { const notifyPath = "/tmp/.superset/hooks/notify.sh"; const content = getCodexGlobalHooksJsonContent(notifyPath); expect(content).not.toBeNull(); @@ -907,8 +907,6 @@ describe("agent-wrappers codex hooks.json", () => { for (const eventName of [ "SessionStart", "UserPromptSubmit", - "PreToolUse", - "PostToolUse", "Stop", ] as const) { const hooks = parsed.hooks[eventName]; @@ -920,12 +918,8 @@ describe("agent-wrappers codex hooks.json", () => { ).toBe(true); } - expect(parsed.hooks.PreToolUse?.every((def) => def.matcher === "*")).toBe( - true, - ); - expect(parsed.hooks.PostToolUse?.every((def) => def.matcher === "*")).toBe( - true, - ); + expect(parsed.hooks.PreToolUse).toBeUndefined(); + expect(parsed.hooks.PostToolUse).toBeUndefined(); }); it("preserves user hooks when merging", () => { @@ -987,7 +981,7 @@ describe("agent-wrappers codex hooks.json", () => { const parsed = JSON.parse(content); - // Preserves user hook + // Preserves user hooks (including PreToolUse/PostToolUse which we don't manage) expect( parsed.hooks.Stop.some((def: { hooks: Array<{ command: string }> }) => def.hooks.some( @@ -1024,32 +1018,19 @@ describe("agent-wrappers codex hooks.json", () => { ), ).toBe(true); - // Adds managed hook - expect( - parsed.hooks.Stop.some((def: { hooks: Array<{ command: string }> }) => - def.hooks.some( - (hook: { command: string }) => hook.command === notifyPath, + // Adds managed hooks for SessionStart, UserPromptSubmit, Stop + for (const eventName of ["SessionStart", "UserPromptSubmit", "Stop"]) { + expect( + parsed.hooks[eventName].some( + (def: { hooks: Array<{ command: string }> }) => + def.hooks.some( + (hook: { command: string }) => hook.command === notifyPath, + ), ), - ), - ).toBe(true); + ).toBe(true); + } - // Also creates prompt + start hooks - expect( - parsed.hooks.SessionStart.some( - (def: { hooks: Array<{ command: string }> }) => - def.hooks.some( - (hook: { command: string }) => hook.command === notifyPath, - ), - ), - ).toBe(true); - expect( - parsed.hooks.UserPromptSubmit.some( - (def: { hooks: Array<{ command: string }> }) => - def.hooks.some( - (hook: { command: string }) => hook.command === notifyPath, - ), - ), - ).toBe(true); + // Does NOT inject managed hooks for PreToolUse/PostToolUse expect( parsed.hooks.PreToolUse.some( (def: { hooks: Array<{ command: string }> }) => @@ -1057,7 +1038,7 @@ describe("agent-wrappers codex hooks.json", () => { (hook: { command: string }) => hook.command === notifyPath, ), ), - ).toBe(true); + ).toBe(false); expect( parsed.hooks.PostToolUse.some( (def: { hooks: Array<{ command: string }> }) => @@ -1065,37 +1046,7 @@ describe("agent-wrappers codex hooks.json", () => { (hook: { command: string }) => hook.command === notifyPath, ), ), - ).toBe(true); - }); - - it("adds UserPromptSubmit, PreToolUse, and PostToolUse to the Codex hooks.json merge", () => { - const notifyPath = "/tmp/.superset/hooks/notify.sh"; - const content = getCodexGlobalHooksJsonContent(notifyPath); - expect(content).not.toBeNull(); - if (content === null) throw new Error("Expected content"); - - const parsed = JSON.parse(content) as { - hooks: Record< - string, - Array<{ - matcher?: string; - hooks: Array<{ type: string; command: string }>; - }> - >; - }; - - for (const eventName of [ - "UserPromptSubmit", - "PreToolUse", - "PostToolUse", - ] as const) { - expect(parsed.hooks[eventName]).toBeDefined(); - expect( - parsed.hooks[eventName]?.some((def) => - def.hooks.some((hook) => hook.command === notifyPath), - ), - ).toBe(true); - } + ).toBe(false); }); it("replaces stale Codex hook commands from old superset paths", () => { @@ -1150,8 +1101,6 @@ describe("agent-wrappers codex hooks.json", () => { for (const eventName of [ "SessionStart", "UserPromptSubmit", - "PreToolUse", - "PostToolUse", "Stop", ] as const) { const hooks = parsed.hooks[eventName]; diff --git a/apps/desktop/src/main/terminal-host/session-shell-ready.test.ts b/apps/desktop/src/main/terminal-host/session-shell-ready.test.ts index a2ad32fabbb..791ef23e10e 100644 --- a/apps/desktop/src/main/terminal-host/session-shell-ready.test.ts +++ b/apps/desktop/src/main/terminal-host/session-shell-ready.test.ts @@ -301,57 +301,6 @@ describe("Session shell-ready: kill/exit before readiness", () => { }); }); -/** Wait for the emulator write queue to drain (uses setImmediate internally). */ -function waitForEmulatorFlush(): Promise { - return new Promise((resolve) => setTimeout(resolve, 50)); -} - -describe("Session shell-ready: DA1 query response forwarding (#3028)", () => { - it("forwards headless emulator DA1 response to subprocess during pending state", async () => { - // Fish shell sends DA1 (ESC[c) at startup to detect terminal capabilities. - // The headless emulator generates a response (ESC[?1;2c) via xterm.js. - // This response MUST be forwarded to the subprocess even during the - // "pending" shell-ready state, otherwise fish waits 10s and then - // disables optional features like cursor shape and reflow detection. - const { session, proc } = createTestSession("/usr/local/bin/fish"); - spawnAndReady(session, proc); - - // Simulate PTY output containing a DA1 query from fish. - // When the headless emulator processes this, xterm.js generates - // a DA1 response via its onData callback. - sendData(proc, "\x1b[c"); - - // The emulator write queue processes via setImmediate, so we need - // to let the event loop tick for xterm to process the query. - await waitForEmulatorFlush(); - - // The emulator's DA1 response should have been forwarded to the - // subprocess (written to stdin) even though shell is still pending. - const writes = getWrittenData(proc); - - // The response should contain a DA1 reply (ESC[?...c format) - expect(writes.length).toBeGreaterThan(0); - const da1Response = writes.join(""); - // biome-ignore lint/suspicious/noControlCharactersInRegex: matching ESC in terminal protocol data - expect(da1Response).toMatch(/\x1b\[\?[\d;]+c/); - }); - - it("forwards DSR response to subprocess during pending state", async () => { - const { session, proc } = createTestSession("/bin/zsh"); - spawnAndReady(session, proc); - - // DSR (Device Status Report): ESC[5n → ESC[0n (terminal OK) - sendData(proc, "\x1b[5n"); - - await waitForEmulatorFlush(); - - const writes = getWrittenData(proc); - expect(writes.length).toBeGreaterThan(0); - const response = writes.join(""); - expect(response).toContain("\x1b[0n"); - }); -}); - describe("Session shell-ready: supported shells", () => { for (const shell of [ "/bin/zsh", diff --git a/apps/desktop/src/main/terminal-host/session.ts b/apps/desktop/src/main/terminal-host/session.ts index 87174902470..6e810b9ad27 100644 --- a/apps/desktop/src/main/terminal-host/session.ts +++ b/apps/desktop/src/main/terminal-host/session.ts @@ -225,20 +225,16 @@ export class Session { this.emulator.setCwd(options.cwd); // The headless emulator responds to terminal queries (e.g. DA1, - // DSR) when no renderer client is attached. These responses must - // be forwarded to the subprocess immediately — even during shell - // init — because shells like fish send DA1 at startup and wait - // up to 10 seconds for a reply before disabling optional features. + // DSR). These responses must be forwarded to the subprocess + // regardless of whether renderer clients are attached, because + // shells like fish send DA1 at startup and wait up to 10 seconds + // for a reply before disabling optional features. // Unlike renderer-generated responses (which go through write() // and are correctly dropped during init to avoid appearing as // typed text), headless emulator responses are written directly // to the PTY and consumed by the shell as protocol data. this.emulator.onData((data) => { - if ( - this.attachedClients.size === 0 && - this.subprocess && - this.subprocessReady - ) { + if (this.subprocess && this.subprocessReady) { this.sendWriteToSubprocess(data); } }); diff --git a/apps/desktop/src/renderer/components/MarkdownRenderer/styles/default/default.css b/apps/desktop/src/renderer/components/MarkdownRenderer/styles/default/default.css index b24a3cde29a..8085dcc3dfe 100644 --- a/apps/desktop/src/renderer/components/MarkdownRenderer/styles/default/default.css +++ b/apps/desktop/src/renderer/components/MarkdownRenderer/styles/default/default.css @@ -48,7 +48,7 @@ .default-markdown p { margin-top: 0; margin-bottom: 1rem; - line-height: 1.7; + line-height: 1.5; } .default-markdown ul, @@ -60,7 +60,7 @@ .default-markdown li { margin-bottom: 0.25rem; - line-height: 1.6; + line-height: 1.5; } .default-markdown ul { diff --git a/apps/desktop/src/renderer/components/MarkdownRenderer/styles/tufte/tufte.css b/apps/desktop/src/renderer/components/MarkdownRenderer/styles/tufte/tufte.css index 5d21f9938a7..edff21d4fcd 100644 --- a/apps/desktop/src/renderer/components/MarkdownRenderer/styles/tufte/tufte.css +++ b/apps/desktop/src/renderer/components/MarkdownRenderer/styles/tufte/tufte.css @@ -2,7 +2,7 @@ .tufte-markdown { font-family: Georgia, "Times New Roman", serif; font-size: 1.1rem; - line-height: 1.8; + line-height: 1.6; color: var(--foreground); } @@ -84,7 +84,7 @@ /* Body text */ .tufte-markdown p { margin-top: 0; - margin-bottom: 1.4rem; + margin-bottom: 1.1rem; text-align: justify; -webkit-hyphens: auto; hyphens: auto; @@ -101,7 +101,7 @@ /* Lists */ .tufte-markdown ul, .tufte-markdown ol { - margin: 1.4rem 0; + margin: 1.1rem 0; padding-left: 1.5rem; list-style-position: outside; } @@ -115,7 +115,8 @@ } .tufte-markdown li { - margin-bottom: 0.5rem; + margin-bottom: 0.35rem; + line-height: 1.5; } /* Links */ diff --git a/apps/desktop/src/renderer/components/Paywall/components/FeaturePreview/components/IntegrationsDemo/IntegrationsDemo.tsx b/apps/desktop/src/renderer/components/Paywall/components/FeaturePreview/components/IntegrationsDemo/IntegrationsDemo.tsx index 1a11b48fd32..1e4c3c26a9e 100644 --- a/apps/desktop/src/renderer/components/Paywall/components/FeaturePreview/components/IntegrationsDemo/IntegrationsDemo.tsx +++ b/apps/desktop/src/renderer/components/Paywall/components/FeaturePreview/components/IntegrationsDemo/IntegrationsDemo.tsx @@ -1,9 +1,15 @@ +import { FaSlack } from "react-icons/fa"; import { HiArrowPath, HiCheck } from "react-icons/hi2"; -import { SiGithub, SiLinear } from "react-icons/si"; +import { SiLinear } from "react-icons/si"; const SYNCED_ITEMS = [ { id: "1", type: "issue", name: "SUP-142: Fix auth flow", status: "synced" }, - { id: "2", type: "pr", name: "PR #89: Add workspace sync", status: "synced" }, + { + id: "2", + type: "message", + name: "#eng: Add workspace sync", + status: "synced", + }, { id: "3", type: "issue", @@ -45,10 +51,10 @@ export function IntegrationsDemo() {
-
- +
+
- GitHub + Slack
diff --git a/apps/desktop/src/renderer/components/Paywall/constants.ts b/apps/desktop/src/renderer/components/Paywall/constants.ts index 2dac4cebb40..0a2c4e540dd 100644 --- a/apps/desktop/src/renderer/components/Paywall/constants.ts +++ b/apps/desktop/src/renderer/components/Paywall/constants.ts @@ -41,7 +41,7 @@ export const PRO_FEATURES: ProFeature[] = [ id: "integrations", title: "Integrations", description: - "Connect Linear, GitHub, and more to sync issues and PRs directly with your workspaces.", + "Connect Linear, Slack, and more to sync issues and conversations directly with your workspaces.", icon: HiOutlinePuzzlePiece, iconColor: "text-purple-500", gradientColors: ["#7c3aed", "#6d28d9", "#4c1d95", "#1a1a2e"], diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/billing/constants.ts b/apps/desktop/src/renderer/routes/_authenticated/settings/billing/constants.ts index 9af00ffa30c..a1deb7ede1d 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/billing/constants.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/billing/constants.ts @@ -48,6 +48,7 @@ export const PLANS: Record = { { id: "workspaces", name: "Up to 5 workspaces", included: true }, { id: "local-only", name: "Local workspaces only", included: true }, { id: "desktop-app", name: "Desktop app", included: true }, + { id: "github", name: "GitHub integration", included: true }, ], cta: { text: "Current plan", action: "current", disabled: true }, }, diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/billing/plans/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/billing/plans/page.tsx index e38003e5022..0cd76ee3dad 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/billing/plans/page.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/billing/plans/page.tsx @@ -136,7 +136,7 @@ const COMPARISON_SECTIONS: ComparisonSection[] = [ }, { label: "GitHub integration", - values: [null, true, true], + values: [true, true, true], }, { label: "Cloud workspaces", @@ -152,6 +152,10 @@ const COMPARISON_SECTIONS: ComparisonSection[] = [ label: "Linear integration", values: [null, true, true], }, + { + label: "Slack integration", + values: [null, true, true], + }, { label: "Team collaboration", values: [null, true, true], diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/integrations/components/IntegrationsSettings/IntegrationsSettings.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/integrations/components/IntegrationsSettings/IntegrationsSettings.tsx index 1ca327fdf92..6eabfde81aa 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/integrations/components/IntegrationsSettings/IntegrationsSettings.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/integrations/components/IntegrationsSettings/IntegrationsSettings.tsx @@ -60,9 +60,6 @@ export function IntegrationsSettings({ useState(null); const [isLoadingGithub, setIsLoadingGithub] = useState(true); - const hasGithubAccess = useFeatureFlagEnabled( - FEATURE_FLAGS.GITHUB_INTEGRATION_ACCESS, - ); const hasSlackAccess = useFeatureFlagEnabled( FEATURE_FLAGS.SLACK_INTEGRATION_ACCESS, ); @@ -71,9 +68,10 @@ export function IntegrationsSettings({ SETTING_ITEM_ID.INTEGRATIONS_LINEAR, visibleItems, ); - const showGithub = - hasGithubAccess && - isItemVisible(SETTING_ITEM_ID.INTEGRATIONS_GITHUB, visibleItems); + const showGithub = isItemVisible( + SETTING_ITEM_ID.INTEGRATIONS_GITHUB, + visibleItems, + ); const fetchGithubInstallation = useCallback(async () => { if (!activeOrganizationId) { @@ -161,11 +159,7 @@ export function IntegrationsSettings({ isConnected={isGithubConnected} connectedOrgName={githubInstallation?.accountLogin} isLoading={isLoadingGithub} - onManage={() => - gateFeature(GATED_FEATURES.INTEGRATIONS, () => - handleOpenWeb("/integrations/github"), - ) - } + onManage={() => handleOpenWeb("/integrations/github")} /> )} diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/hooks/useTerminalLifecycle.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/hooks/useTerminalLifecycle.ts index 034319ab923..c4ed578effa 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/hooks/useTerminalLifecycle.ts +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/hooks/useTerminalLifecycle.ts @@ -599,6 +599,20 @@ export function useTerminalLifecycle({ return; } + requestAnimationFrame(() => { + if (!isAttachActive()) return; + const prevCols = xterm.cols; + const prevRows = xterm.rows; + fitAddon.fit(); + if (xterm.cols !== prevCols || xterm.rows !== prevRows) { + resizeRef.current({ + paneId, + cols: xterm.cols, + rows: xterm.rows, + }); + } + }); + pendingInitialStateRef.current = result; maybeApplyInitialState(); diff --git a/apps/desktop/src/renderer/stores/tabs/store.ts b/apps/desktop/src/renderer/stores/tabs/store.ts index aaaea777863..3f7539cd779 100644 --- a/apps/desktop/src/renderer/stores/tabs/store.ts +++ b/apps/desktop/src/renderer/stores/tabs/store.ts @@ -782,19 +782,18 @@ export const useTabsStore = create()( const tabPaneIds = extractPaneIdsFromLayout(activeTab.layout); const reuseExisting = options.reuseExisting ?? "workspace"; - const canReuseExistingPane = - !options.openInNewTab && reuseExisting !== "none"; - const existingFileViewerPane = canReuseExistingPane - ? findReusableFileViewerPane({ - workspaceId, - activeTabId: activeTab.id, - tabs: state.tabs, - panes: state.panes, - tabHistoryStacks: state.tabHistoryStacks, - reuseExisting, - options, - }) - : null; + const existingFileViewerPane = + reuseExisting !== "none" + ? findReusableFileViewerPane({ + workspaceId, + activeTabId: activeTab.id, + tabs: state.tabs, + panes: state.panes, + tabHistoryStacks: state.tabHistoryStacks, + reuseExisting, + options, + }) + : null; if (existingFileViewerPane) { const nextPane = applyFileViewerOpenOptionsToPane( @@ -854,7 +853,11 @@ export const useTabsStore = create()( // If we found an unpinned (preview) file-viewer pane, reuse it // (skip reuse when explicitly requesting a new tab, e.g. cmd+click) - if (fileViewerPanes.length > 0 && canReuseExistingPane) { + if ( + fileViewerPanes.length > 0 && + !options.openInNewTab && + reuseExisting !== "none" + ) { const paneToReuse = fileViewerPanes[0]; const existingFileViewer = paneToReuse.fileViewer; if (!existingFileViewer) { diff --git a/apps/marketing/package.json b/apps/marketing/package.json index fc4221af181..53d6ea3e2c4 100644 --- a/apps/marketing/package.json +++ b/apps/marketing/package.json @@ -25,7 +25,7 @@ "import-in-the-middle": "2.0.1", "lucide-react": "^0.563.0", "next": "^16.0.10", - "next-mdx-remote": "^5.0.0", + "next-mdx-remote": "^6.0.0", "next-themes": "^0.4.6", "posthog-js": "1.310.1", "react": "19.2.0", diff --git a/apps/web/src/app/(dashboard-legacy)/integrations/page.tsx b/apps/web/src/app/(dashboard-legacy)/integrations/page.tsx index 351d3daf5d1..981e7b0a1f4 100644 --- a/apps/web/src/app/(dashboard-legacy)/integrations/page.tsx +++ b/apps/web/src/app/(dashboard-legacy)/integrations/page.tsx @@ -38,9 +38,6 @@ const integrations: IntegrationCardProps[] = [ ]; export default function IntegrationsPage() { - const hasGithubAccess = useFeatureFlagEnabled( - FEATURE_FLAGS.GITHUB_INTEGRATION_ACCESS, - ); const hasSlackAccess = useFeatureFlagEnabled( FEATURE_FLAGS.SLACK_INTEGRATION_ACCESS, ); @@ -48,11 +45,10 @@ export default function IntegrationsPage() { const visibleIntegrations = useMemo( () => integrations.filter((i) => { - if (i.id === "github") return hasGithubAccess; if (i.id === "slack") return hasSlackAccess; return true; }), - [hasGithubAccess, hasSlackAccess], + [hasSlackAccess], ); return ( diff --git a/bun.lock b/bun.lock index 1097a96863a..55e56802374 100644 --- a/bun.lock +++ b/bun.lock @@ -110,7 +110,7 @@ }, "apps/desktop": { "name": "@superset/desktop", - "version": "1.4.6", + "version": "1.4.7", "dependencies": { "@ai-sdk/anthropic": "^3.0.43", "@ai-sdk/openai": "3.0.36", @@ -425,7 +425,7 @@ "import-in-the-middle": "2.0.1", "lucide-react": "^0.563.0", "next": "^16.0.10", - "next-mdx-remote": "^5.0.0", + "next-mdx-remote": "^6.0.0", "next-themes": "^0.4.6", "posthog-js": "1.310.1", "react": "19.2.0", @@ -801,7 +801,6 @@ "@superset/ui": "workspace:*", "lucide-react": "^0.563.0", "react-dnd": "^16.0.1", - "react-dnd-html5-backend": "^16.0.1", "zustand": "^5.0.8", }, "devDependencies": { @@ -4684,7 +4683,7 @@ "next": ["next@16.2.1", "", { "dependencies": { "@next/env": "16.2.1", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.2.1", "@next/swc-darwin-x64": "16.2.1", "@next/swc-linux-arm64-gnu": "16.2.1", "@next/swc-linux-arm64-musl": "16.2.1", "@next/swc-linux-x64-gnu": "16.2.1", "@next/swc-linux-x64-musl": "16.2.1", "@next/swc-win32-arm64-msvc": "16.2.1", "@next/swc-win32-x64-msvc": "16.2.1", "sharp": "^0.34.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-VaChzNL7o9rbfdt60HUj8tev4m6d7iC1igAy157526+cJlXOQu5LzsBXNT+xaJnTP/k+utSX5vMv7m0G+zKH+Q=="], - "next-mdx-remote": ["next-mdx-remote@5.0.0", "", { "dependencies": { "@babel/code-frame": "^7.23.5", "@mdx-js/mdx": "^3.0.1", "@mdx-js/react": "^3.0.1", "unist-util-remove": "^3.1.0", "vfile": "^6.0.1", "vfile-matter": "^5.0.0" }, "peerDependencies": { "react": ">=16" } }, "sha512-RNNbqRpK9/dcIFZs/esQhuLA8jANqlH694yqoDBK8hkVdJUndzzGmnPHa2nyi90N4Z9VmzuSWNRpr5ItT3M7xQ=="], + "next-mdx-remote": ["next-mdx-remote@6.0.0", "", { "dependencies": { "@babel/code-frame": "^7.23.5", "@mdx-js/mdx": "^3.0.1", "@mdx-js/react": "^3.0.1", "unist-util-remove": "^4.0.0", "unist-util-visit": "^5.1.0", "vfile": "^6.0.1", "vfile-matter": "^5.0.0" }, "peerDependencies": { "react": ">=16" } }, "sha512-cJEpEZlgD6xGjB4jL8BnI8FaYdN9BzZM4NwadPe1YQr7pqoWjg9EBCMv3nXBkuHqMRfv2y33SzUsuyNh9LFAQQ=="], "next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="], @@ -5678,7 +5677,7 @@ "unist-util-position-from-estree": ["unist-util-position-from-estree@2.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ=="], - "unist-util-remove": ["unist-util-remove@3.1.1", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^5.0.0", "unist-util-visit-parents": "^5.0.0" } }, "sha512-kfCqZK5YVY5yEa89tvpl7KnBBHu2c6CzMkqHUrlOqaRgGOMp0sMvwWOVrbAtj03KhovQB7i96Gda72v/EFE0vw=="], + "unist-util-remove": ["unist-util-remove@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-b4gokeGId57UVRX/eVKej5gXqGlc9+trkORhFJpu9raqZkZhU0zm8Doi05+HaiBsMEIJowL+2WtQ5ItjsngPXg=="], "unist-util-remove-position": ["unist-util-remove-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q=="], @@ -6744,12 +6743,6 @@ "tunnel-rat/zustand": ["zustand@4.5.7", "", { "dependencies": { "use-sync-external-store": "^1.2.2" }, "peerDependencies": { "@types/react": ">=16.8", "immer": ">=9.0.6", "react": ">=16.8" }, "optionalPeers": ["@types/react", "immer", "react"] }, "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw=="], - "unist-util-remove/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], - - "unist-util-remove/unist-util-is": ["unist-util-is@5.2.1", "", { "dependencies": { "@types/unist": "^2.0.0" } }, "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw=="], - - "unist-util-remove/unist-util-visit-parents": ["unist-util-visit-parents@5.1.3", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^5.0.0" } }, "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg=="], - "uniwind/@tailwindcss/node": ["@tailwindcss/node@4.2.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.31.1", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.1" } }, "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg=="], "uniwind/@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.1", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.1", "@tailwindcss/oxide-darwin-arm64": "4.2.1", "@tailwindcss/oxide-darwin-x64": "4.2.1", "@tailwindcss/oxide-freebsd-x64": "4.2.1", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", "@tailwindcss/oxide-linux-x64-musl": "4.2.1", "@tailwindcss/oxide-wasm32-wasi": "4.2.1", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" } }, "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw=="], diff --git a/packages/panes/README.md b/packages/panes/README.md index dce39c6aba6..dc78379e938 100644 --- a/packages/panes/README.md +++ b/packages/panes/README.md @@ -330,6 +330,25 @@ Pin from inside a pane component (e.g. on first edit): context.actions.pin(); ``` +## Drag-and-Drop + +`Workspace` uses `react-dnd` internally for tab reordering and pane dragging but does **not** include its own `DndProvider`. You must wrap `` in a `DndProvider` yourself: + +```tsx +import { DndProvider } from "react-dnd"; +import { HTML5Backend } from "react-dnd-html5-backend"; + +function App() { + return ( + + + + ); +} +``` + +This is intentional — embedding a provider inside `Workspace` would conflict with any parent `DndProvider` in your app (the HTML5 backend cannot be instantiated twice). Keeping it external lets you share a single backend across your entire component tree. + ## Workspace Props ```ts diff --git a/packages/panes/package.json b/packages/panes/package.json index fea2ba390d5..5640d099d14 100644 --- a/packages/panes/package.json +++ b/packages/panes/package.json @@ -18,7 +18,6 @@ "@superset/ui": "workspace:*", "lucide-react": "^0.563.0", "react-dnd": "^16.0.1", - "react-dnd-html5-backend": "^16.0.1", "zustand": "^5.0.8" }, "devDependencies": { diff --git a/packages/panes/src/react/components/Workspace/Workspace.tsx b/packages/panes/src/react/components/Workspace/Workspace.tsx index dbe07c57998..9d35efeaf41 100644 --- a/packages/panes/src/react/components/Workspace/Workspace.tsx +++ b/packages/panes/src/react/components/Workspace/Workspace.tsx @@ -1,6 +1,4 @@ import { cn } from "@superset/ui/utils"; -import { DndProvider } from "react-dnd"; -import { HTML5Backend } from "react-dnd-html5-backend"; import { useStore } from "zustand"; import type { WorkspaceProps } from "../../types"; import { Tab } from "./components/Tab"; @@ -31,53 +29,49 @@ export function Workspace({ }; return ( - -
- store.getState().setActiveTab(tabId)} - onCloseTab={closeTab} - onCloseOtherTabs={(tabId) => { - for (const tab of tabs) { - if (tab.id !== tabId) closeTab(tab.id); - } - }} - onCloseAllTabs={() => { - for (const tab of tabs) { - closeTab(tab.id); - } - }} - onRenameTab={(tabId, title) => - store - .getState() - .setTabTitleOverride({ tabId, titleOverride: title }) +
+ store.getState().setActiveTab(tabId)} + onCloseTab={closeTab} + onCloseOtherTabs={async (tabId) => { + for (const tab of tabs) { + if (tab.id !== tabId) await closeTab(tab.id); } - onReorderTab={(tabId, toIndex) => - store.getState().reorderTab({ tabId, toIndex }) + }} + onCloseAllTabs={async () => { + for (const tab of tabs) { + await closeTab(tab.id); } - getTabTitle={(tab) => tab.titleOverride ?? tab.id} - renderAddTabMenu={renderAddTabMenu} - renderTabAccessory={renderTabAccessory} + }} + onRenameTab={(tabId, title) => + store.getState().setTabTitleOverride({ tabId, titleOverride: title }) + } + onReorderTab={(tabId, toIndex) => + store.getState().reorderTab({ tabId, toIndex }) + } + getTabTitle={(tab) => tab.titleOverride ?? tab.id} + renderAddTabMenu={renderAddTabMenu} + renderTabAccessory={renderTabAccessory} + /> + {activeTab ? ( + - {activeTab ? ( - - ) : ( -
- {renderEmptyState?.() ?? "No tabs open"} -
- )} -
- + ) : ( +
+ {renderEmptyState?.() ?? "No tabs open"} +
+ )} +
); }