diff --git a/apps/desktop/src/lib/trpc/routers/settings/index.ts b/apps/desktop/src/lib/trpc/routers/settings/index.ts
index cc4d47661bd..9fae5170f8f 100644
--- a/apps/desktop/src/lib/trpc/routers/settings/index.ts
+++ b/apps/desktop/src/lib/trpc/routers/settings/index.ts
@@ -158,6 +158,47 @@ export const createSettingsRouter = () => {
return { success: true };
}),
+ reorderTerminalPresets: publicProcedure
+ .input(
+ z.object({
+ presetId: z.string(),
+ targetIndex: z.number().int().min(0),
+ }),
+ )
+ .mutation(({ input }) => {
+ const row = getSettings();
+ const presets = row.terminalPresets ?? [];
+
+ const currentIndex = presets.findIndex((p) => p.id === input.presetId);
+ if (currentIndex === -1) {
+ throw new TRPCError({
+ code: "NOT_FOUND",
+ message: "Preset not found",
+ });
+ }
+
+ if (input.targetIndex < 0 || input.targetIndex >= presets.length) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Invalid target index for reordering presets",
+ });
+ }
+
+ const [removed] = presets.splice(currentIndex, 1);
+ presets.splice(input.targetIndex, 0, removed);
+
+ localDb
+ .insert(settings)
+ .values({ id: 1, terminalPresets: presets })
+ .onConflictDoUpdate({
+ target: settings.id,
+ set: { terminalPresets: presets },
+ })
+ .run();
+
+ return { success: true };
+ }),
+
getDefaultPreset: publicProcedure.query(() => {
const row = getSettings();
const presets = row.terminalPresets ?? [];
diff --git a/apps/desktop/src/renderer/react-query/presets/index.ts b/apps/desktop/src/renderer/react-query/presets/index.ts
index 97ace8013e1..a2c2b888985 100644
--- a/apps/desktop/src/renderer/react-query/presets/index.ts
+++ b/apps/desktop/src/renderer/react-query/presets/index.ts
@@ -65,6 +65,22 @@ function useSetDefaultPreset(
});
}
+function useReorderTerminalPresets(
+ options?: Parameters<
+ typeof electronTrpc.settings.reorderTerminalPresets.useMutation
+ >[0],
+) {
+ const utils = electronTrpc.useUtils();
+
+ return electronTrpc.settings.reorderTerminalPresets.useMutation({
+ ...options,
+ onSuccess: async (...args) => {
+ await utils.settings.getTerminalPresets.invalidate();
+ await options?.onSuccess?.(...args);
+ },
+ });
+}
+
/**
* Combined hook for accessing terminal presets with all CRUD operations
* Provides easy access to presets data and mutations from anywhere in the app
@@ -80,6 +96,7 @@ export function usePresets() {
const updatePreset = useUpdateTerminalPreset();
const deletePreset = useDeleteTerminalPreset();
const setDefaultPreset = useSetDefaultPreset();
+ const reorderPresets = useReorderTerminalPresets();
return {
presets,
@@ -89,5 +106,6 @@ export function usePresets() {
updatePreset,
deletePreset,
setDefaultPreset,
+ reorderPresets,
};
}
diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/workspace/$workspaceId/hooks/usePresetHotkeys.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/workspace/$workspaceId/hooks/usePresetHotkeys.ts
new file mode 100644
index 00000000000..c0e96233167
--- /dev/null
+++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/workspace/$workspaceId/hooks/usePresetHotkeys.ts
@@ -0,0 +1,46 @@
+import { useAppHotkey } from "renderer/stores/hotkeys";
+import type { HotkeyId } from "shared/hotkeys";
+
+const PRESET_HOTKEY_IDS: HotkeyId[] = [
+ "OPEN_PRESET_1",
+ "OPEN_PRESET_2",
+ "OPEN_PRESET_3",
+ "OPEN_PRESET_4",
+ "OPEN_PRESET_5",
+ "OPEN_PRESET_6",
+ "OPEN_PRESET_7",
+ "OPEN_PRESET_8",
+ "OPEN_PRESET_9",
+];
+
+export function usePresetHotkeys(
+ openTabWithPreset: (presetIndex: number) => void,
+) {
+ useAppHotkey(PRESET_HOTKEY_IDS[0], () => openTabWithPreset(0), undefined, [
+ openTabWithPreset,
+ ]);
+ useAppHotkey(PRESET_HOTKEY_IDS[1], () => openTabWithPreset(1), undefined, [
+ openTabWithPreset,
+ ]);
+ useAppHotkey(PRESET_HOTKEY_IDS[2], () => openTabWithPreset(2), undefined, [
+ openTabWithPreset,
+ ]);
+ useAppHotkey(PRESET_HOTKEY_IDS[3], () => openTabWithPreset(3), undefined, [
+ openTabWithPreset,
+ ]);
+ useAppHotkey(PRESET_HOTKEY_IDS[4], () => openTabWithPreset(4), undefined, [
+ openTabWithPreset,
+ ]);
+ useAppHotkey(PRESET_HOTKEY_IDS[5], () => openTabWithPreset(5), undefined, [
+ openTabWithPreset,
+ ]);
+ useAppHotkey(PRESET_HOTKEY_IDS[6], () => openTabWithPreset(6), undefined, [
+ openTabWithPreset,
+ ]);
+ useAppHotkey(PRESET_HOTKEY_IDS[7], () => openTabWithPreset(7), undefined, [
+ openTabWithPreset,
+ ]);
+ useAppHotkey(PRESET_HOTKEY_IDS[8], () => openTabWithPreset(8), undefined, [
+ openTabWithPreset,
+ ]);
+}
diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/workspace/$workspaceId/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/workspace/$workspaceId/page.tsx
index 48a62006de6..410abb02656 100644
--- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/workspace/$workspaceId/page.tsx
+++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/workspace/$workspaceId/page.tsx
@@ -2,7 +2,9 @@ import { createFileRoute, notFound, useNavigate } from "@tanstack/react-router";
import { useCallback, useMemo } from "react";
import { electronTrpc } from "renderer/lib/electron-trpc";
import { electronTrpcClient as trpcClient } from "renderer/lib/trpc-client";
+import { usePresets } from "renderer/react-query/presets";
import { navigateToWorkspace } from "renderer/routes/_authenticated/_dashboard/utils/workspace-navigation";
+import { usePresetHotkeys } from "renderer/routes/_authenticated/_dashboard/workspace/$workspaceId/hooks/usePresetHotkeys";
import { NotFound } from "renderer/routes/not-found";
import { WorkspaceInitializingView } from "renderer/screens/main/components/WorkspaceView/WorkspaceInitializingView";
import { WorkspaceLayout } from "renderer/screens/main/components/WorkspaceView/WorkspaceLayout";
@@ -111,16 +113,33 @@ function WorkspacePage() {
const focusedPaneId = activeTabId ? focusedPaneIds[activeTabId] : null;
- // Tab management shortcuts
- useAppHotkey(
- "NEW_GROUP",
- () => {
- addTab(workspaceId);
+ const { presets } = usePresets();
+ const renameTab = useTabsStore((s) => s.renameTab);
+
+ const openTabWithPreset = useCallback(
+ (presetIndex: number) => {
+ const preset = presets[presetIndex];
+ if (preset) {
+ const result = addTab(workspaceId, {
+ initialCommands: preset.commands,
+ initialCwd: preset.cwd || undefined,
+ });
+ if (preset.name) {
+ renameTab(result.tabId, preset.name);
+ }
+ } else {
+ addTab(workspaceId);
+ }
},
- undefined,
- [workspaceId, addTab],
+ [presets, workspaceId, addTab, renameTab],
);
+ useAppHotkey("NEW_GROUP", () => addTab(workspaceId), undefined, [
+ workspaceId,
+ addTab,
+ ]);
+ usePresetHotkeys(openTabWithPreset);
+
useAppHotkey(
"CLOSE_TERMINAL",
() => {
@@ -132,9 +151,8 @@ function WorkspacePage() {
[focusedPaneId, removePane],
);
- // Switch between tabs
useAppHotkey(
- "PREV_TERMINAL",
+ "PREV_TAB",
() => {
if (!activeTabId) return;
const index = tabs.findIndex((t) => t.id === activeTabId);
@@ -147,7 +165,7 @@ function WorkspacePage() {
);
useAppHotkey(
- "NEXT_TERMINAL",
+ "NEXT_TAB",
() => {
if (!activeTabId) return;
const index = tabs.findIndex((t) => t.id === activeTabId);
@@ -159,7 +177,6 @@ function WorkspacePage() {
[workspaceId, activeTabId, tabs, setActiveTab],
);
- // Switch between panes within a tab
useAppHotkey(
"PREV_PANE",
() => {
diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/keyboard/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/keyboard/page.tsx
index 2d0a72f6361..aa00a30efd9 100644
--- a/apps/desktop/src/renderer/routes/_authenticated/settings/keyboard/page.tsx
+++ b/apps/desktop/src/renderer/routes/_authenticated/settings/keyboard/page.tsx
@@ -121,19 +121,18 @@ function KeyboardShortcutsPage() {
const showHotkeysDisplay = useHotkeyDisplay("SHOW_HOTKEYS");
- const allHotkeys = useMemo(
- () =>
- CATEGORY_ORDER.flatMap((category) => hotkeysByCategory[category] ?? []),
- [hotkeysByCategory],
- );
-
- const filteredHotkeys = useMemo(() => {
- if (!searchQuery) return allHotkeys;
+ const filteredHotkeysByCategory = useMemo(() => {
+ if (!searchQuery) return hotkeysByCategory;
const lower = searchQuery.toLowerCase();
- return allHotkeys.filter((hotkey) =>
- hotkey.label.toLowerCase().includes(lower),
- );
- }, [allHotkeys, searchQuery]);
+ return Object.fromEntries(
+ CATEGORY_ORDER.map((category) => [
+ category,
+ (hotkeysByCategory[category] ?? []).filter((hotkey) =>
+ hotkey.label.toLowerCase().includes(lower),
+ ),
+ ]),
+ ) as typeof hotkeysByCategory;
+ }, [hotkeysByCategory, searchQuery]);
useEffect(() => {
if (!recordingId) return;
@@ -296,36 +295,51 @@ function KeyboardShortcutsPage() {
/>
- {/* Table */}
-
-
-
- Command
-
-
- Shortcut
-
-
+ {/* Tables by Category */}
+
+ {CATEGORY_ORDER.map((category) => {
+ const hotkeys = filteredHotkeysByCategory[category] ?? [];
+ if (hotkeys.length === 0) return null;
-
- {filteredHotkeys.length > 0 ? (
- filteredHotkeys.map((hotkey) => (
-
handleStartRecording(hotkey.id)}
- onReset={() => resetHotkey(hotkey.id)}
- />
- ))
- ) : (
-
- No shortcuts found matching "{searchQuery}"
+ return (
+
+
+ {category}
+
+
+
+
+ Command
+
+
+ Shortcut
+
+
+
+ {hotkeys.map((hotkey) => (
+ handleStartRecording(hotkey.id)}
+ onReset={() => resetHotkey(hotkey.id)}
+ />
+ ))}
+
+
- )}
-
+ );
+ })}
+
+ {CATEGORY_ORDER.every(
+ (cat) => (filteredHotkeysByCategory[cat] ?? []).length === 0,
+ ) && (
+
+ No shortcuts found matching "{searchQuery}"
+
+ )}
{/* Conflict dialog */}
diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/TerminalSettings.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/TerminalSettings.tsx
index 2279cdb9478..b7ef342bb51 100644
--- a/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/TerminalSettings.tsx
+++ b/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/TerminalSettings.tsx
@@ -23,7 +23,7 @@ import {
import { toast } from "@superset/ui/sonner";
import { Switch } from "@superset/ui/switch";
import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip";
-import { useEffect, useMemo, useRef, useState } from "react";
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
HiOutlineCheck,
HiOutlinePlus,
@@ -145,11 +145,17 @@ export function TerminalSettings({ visibleItems }: TerminalSettingsProps) {
updatePreset,
deletePreset,
setDefaultPreset,
+ reorderPresets,
} = usePresets();
const [localPresets, setLocalPresets] =
useState
(serverPresets);
const presetsContainerRef = useRef(null);
const prevPresetsCountRef = useRef(serverPresets.length);
+ const serverPresetsRef = useRef(serverPresets);
+
+ useEffect(() => {
+ serverPresetsRef.current = serverPresets;
+ }, [serverPresets]);
useEffect(() => {
setLocalPresets(serverPresets);
@@ -173,97 +179,159 @@ export function TerminalSettings({ visibleItems }: TerminalSettingsProps) {
const isTemplateAdded = (template: PresetTemplate) =>
existingPresetNames.has(template.preset.name);
- const handleCellChange = (
- rowIndex: number,
- column: PresetColumnKey,
- value: string,
- ) => {
- setLocalPresets((prev) =>
- prev.map((p, i) => (i === rowIndex ? { ...p, [column]: value } : p)),
- );
- };
+ const handleCellChange = useCallback(
+ (rowIndex: number, column: PresetColumnKey, value: string) => {
+ setLocalPresets((prev) =>
+ prev.map((p, i) => (i === rowIndex ? { ...p, [column]: value } : p)),
+ );
+ },
+ [],
+ );
- const handleCellBlur = (rowIndex: number, column: PresetColumnKey) => {
- const preset = localPresets[rowIndex];
- const serverPreset = serverPresets[rowIndex];
- if (!preset || !serverPreset) return;
- if (preset[column] === serverPreset[column]) return;
+ const handleCellBlur = useCallback(
+ (rowIndex: number, column: PresetColumnKey) => {
+ setLocalPresets((currentLocal) => {
+ const preset = currentLocal[rowIndex];
+ if (!preset) return currentLocal;
+ const serverPreset = serverPresetsRef.current.find(
+ (p) => p.id === preset.id,
+ );
+ if (!serverPreset) return currentLocal;
+ if (preset[column] === serverPreset[column]) return currentLocal;
- updatePreset.mutate({
- id: preset.id,
- patch: { [column]: preset[column] },
- });
- };
+ updatePreset.mutate({
+ id: preset.id,
+ patch: { [column]: preset[column] },
+ });
+ return currentLocal;
+ });
+ },
+ [updatePreset],
+ );
- const handleCommandsChange = (rowIndex: number, commands: string[]) => {
- const preset = localPresets[rowIndex];
- const isDelete = preset && commands.length < preset.commands.length;
+ const handleCommandsChange = useCallback(
+ (rowIndex: number, commands: string[]) => {
+ setLocalPresets((prev) => {
+ const preset = prev[rowIndex];
+ const isDelete = preset && commands.length < preset.commands.length;
+ const newPresets = prev.map((p, i) =>
+ i === rowIndex ? { ...p, commands } : p,
+ );
- setLocalPresets((prev) =>
- prev.map((p, i) => (i === rowIndex ? { ...p, commands } : p)),
- );
+ // Save immediately on delete since onBlur won't have the updated state yet
+ if (isDelete && preset) {
+ updatePreset.mutate({
+ id: preset.id,
+ patch: { commands },
+ });
+ }
+ return newPresets;
+ });
+ },
+ [updatePreset],
+ );
+
+ const handleCommandsBlur = useCallback(
+ (rowIndex: number) => {
+ setLocalPresets((currentLocal) => {
+ const preset = currentLocal[rowIndex];
+ if (!preset) return currentLocal;
+ const serverPreset = serverPresetsRef.current.find(
+ (p) => p.id === preset.id,
+ );
+ if (!serverPreset) return currentLocal;
+ if (
+ JSON.stringify(preset.commands) ===
+ JSON.stringify(serverPreset.commands)
+ )
+ return currentLocal;
- // Save immediately on delete since onBlur won't have the updated state yet
- if (isDelete) {
- updatePreset.mutate({
- id: preset.id,
- patch: { commands },
+ updatePreset.mutate({
+ id: preset.id,
+ patch: { commands: preset.commands },
+ });
+ return currentLocal;
});
- }
- };
+ },
+ [updatePreset],
+ );
- const handleCommandsBlur = (rowIndex: number) => {
- const preset = localPresets[rowIndex];
- const serverPreset = serverPresets[rowIndex];
- if (!preset || !serverPreset) return;
- if (
- JSON.stringify(preset.commands) === JSON.stringify(serverPreset.commands)
- )
- return;
-
- updatePreset.mutate({
- id: preset.id,
- patch: { commands: preset.commands },
- });
- };
+ const handleExecutionModeChange = useCallback(
+ (rowIndex: number, mode: ExecutionMode) => {
+ setLocalPresets((currentLocal) => {
+ const preset = currentLocal[rowIndex];
+ if (!preset) return currentLocal;
- const handleExecutionModeChange = (rowIndex: number, mode: ExecutionMode) => {
- const preset = localPresets[rowIndex];
- if (!preset) return;
+ const newPresets = currentLocal.map((p, i) =>
+ i === rowIndex ? { ...p, executionMode: mode } : p,
+ );
- setLocalPresets((prev) =>
- prev.map((p, i) => (i === rowIndex ? { ...p, executionMode: mode } : p)),
- );
+ updatePreset.mutate({
+ id: preset.id,
+ patch: { executionMode: mode },
+ });
- updatePreset.mutate({
- id: preset.id,
- patch: { executionMode: mode },
- });
- };
+ return newPresets;
+ });
+ },
+ [updatePreset],
+ );
- const handleAddRow = () => {
+ const handleAddRow = useCallback(() => {
createPreset.mutate({
name: "",
cwd: "",
commands: [""],
});
- };
+ }, [createPreset]);
- const handleAddTemplate = (template: PresetTemplate) => {
- if (isTemplateAdded(template)) return;
- createPreset.mutate(template.preset);
- };
+ const handleAddTemplate = useCallback(
+ (template: PresetTemplate) => {
+ if (existingPresetNames.has(template.preset.name)) return;
+ createPreset.mutate(template.preset);
+ },
+ [createPreset, existingPresetNames],
+ );
- const handleDeleteRow = (rowIndex: number) => {
- const preset = localPresets[rowIndex];
- if (!preset) return;
+ const handleDeleteRow = useCallback(
+ (rowIndex: number) => {
+ setLocalPresets((currentLocal) => {
+ const preset = currentLocal[rowIndex];
+ if (preset) {
+ deletePreset.mutate({ id: preset.id });
+ }
+ return currentLocal;
+ });
+ },
+ [deletePreset],
+ );
- deletePreset.mutate({ id: preset.id });
- };
+ const handleSetDefault = useCallback(
+ (presetId: string | null) => {
+ setDefaultPreset.mutate({ id: presetId });
+ },
+ [setDefaultPreset],
+ );
+
+ const handleLocalReorder = useCallback(
+ (fromIndex: number, toIndex: number) => {
+ setLocalPresets((prev) => {
+ const newPresets = [...prev];
+ const [removed] = newPresets.splice(fromIndex, 1);
+ newPresets.splice(toIndex, 0, removed);
+ return newPresets;
+ });
+ },
+ [],
+ );
+
+ const handlePersistReorder = useCallback(
+ (presetId: string, targetIndex: number) => {
+ reorderPresets.mutate({ presetId, targetIndex });
+ },
+ [reorderPresets],
+ );
- const handleSetDefault = (presetId: string | null) => {
- setDefaultPreset.mutate({ id: presetId });
- };
const { data: terminalPersistence, isLoading } =
electronTrpc.settings.getTerminalPersistence.useQuery();
@@ -522,6 +590,7 @@ export function TerminalSettings({ visibleItems }: TerminalSettingsProps) {
{showPresets && (
+
{PRESET_COLUMNS.map((column) => (
))
) : (
diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/components/PresetRow/PresetRow.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/components/PresetRow/PresetRow.tsx
index d2a3b20c923..785d5e298db 100644
--- a/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/components/PresetRow/PresetRow.tsx
+++ b/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/components/PresetRow/PresetRow.tsx
@@ -9,8 +9,10 @@ import {
SelectValue,
} from "@superset/ui/select";
import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip";
+import { useRef } from "react";
+import { useDrag, useDrop } from "react-dnd";
import { HiOutlineStar, HiStar } from "react-icons/hi2";
-import { LuTrash } from "react-icons/lu";
+import { LuGripVertical, LuTrash } from "react-icons/lu";
import {
PRESET_COLUMNS,
type PresetColumnConfig,
@@ -19,6 +21,8 @@ import {
} from "renderer/routes/_authenticated/settings/presets/types";
import { CommandsEditor } from "./components/CommandsEditor";
+const PRESET_TYPE = "TERMINAL_PRESET";
+
interface PresetCellProps {
column: PresetColumnConfig;
preset: TerminalPreset;
@@ -74,6 +78,8 @@ interface PresetRowProps {
onExecutionModeChange: (rowIndex: number, mode: ExecutionMode) => void;
onDelete: (rowIndex: number) => void;
onSetDefault: (presetId: string | null) => void;
+ onLocalReorder: (fromIndex: number, toIndex: number) => void;
+ onPersistReorder: (presetId: string, targetIndex: number) => void;
}
export function PresetRow({
@@ -87,18 +93,58 @@ export function PresetRow({
onExecutionModeChange,
onDelete,
onSetDefault,
+ onLocalReorder,
+ onPersistReorder,
}: PresetRowProps) {
+ const rowRef = useRef
(null);
+ const dragHandleRef = useRef(null);
+
+ const [{ isDragging }, drag, preview] = useDrag(
+ () => ({
+ type: PRESET_TYPE,
+ item: { id: preset.id, index: rowIndex, originalIndex: rowIndex },
+ collect: (monitor) => ({
+ isDragging: monitor.isDragging(),
+ }),
+ }),
+ [preset.id, rowIndex],
+ );
+
+ const [, drop] = useDrop({
+ accept: PRESET_TYPE,
+ hover: (item: { id: string; index: number; originalIndex: number }) => {
+ if (item.index !== rowIndex) {
+ onLocalReorder(item.index, rowIndex);
+ item.index = rowIndex;
+ }
+ },
+ drop: (item: { id: string; index: number; originalIndex: number }) => {
+ if (item.originalIndex !== item.index) {
+ onPersistReorder(item.id, item.index);
+ }
+ },
+ });
+
+ preview(drop(rowRef));
+ drag(dragHandleRef);
+
const handleToggleDefault = () => {
- // If already default, clear it; otherwise set this preset as default
onSetDefault(preset.isDefault ? null : preset.id);
};
return (
+
+
+
{PRESET_COLUMNS.map((column) => (