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
6 changes: 3 additions & 3 deletions src/components/Learn/tips.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@
{
"id": "auto-layout",
"category": "Shortcuts",
"title": "Auto-layout (Cmd/Alt+Shift+L) with four algorithms",
"body": "Cmd/Alt+Shift+L re-arranges the whole graph using the Sugiyama layout. Open View → Auto-layout for the full set: Sugiyama, Sugiyama Centered, Compact, and Spread. Different graph shapes look better under different ones, so it's worth trying them all."
"title": "Auto-layout (Cmd/Ctrl+Shift+L) with four algorithms",
"body": "Cmd/Ctrl+Shift+L re-arranges the whole graph using the Sugiyama layout. Open View → Auto-layout for the full set: Sugiyama, Sugiyama Centered, Compact, and Spread. Different graph shapes look better under different ones, so it's worth trying them all."
},
{
"id": "multi-select-toolbar",
Expand Down Expand Up @@ -99,7 +99,7 @@
"id": "customizable-window-layout",
"category": "Editor",
"title": "Lay the editor out the way you actually work",
"body": "Every side panel is a window you can drag, dock left or right, undock to float, resize, minimize, or maximize. The Windows menu toggles each one on or off and includes a Views submenu with three presets: Default, All, and Minimal. Cmd/Alt+D snaps everything back to the Default layout, and your customizations persist across sessions so you only set things up once."
"body": "Every side panel is a window you can drag, dock left or right, undock to float, resize, minimize, or maximize. The Windows menu toggles each one on or off and includes a Views submenu with three presets: Default, All, and Minimal. Cmd/Ctrl+Shift+D snaps everything back to the Default layout, and your customizations persist across sessions so you only set things up once."
},
{
"id": "recent-runs-window",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { fireEvent, render, screen } from "@testing-library/react";
import type { ReactNode } from "react";
import { beforeEach, describe, expect, it, vi } from "vitest";

import { SharedStoreProvider } from "@/routes/v2/shared/store/SharedStoreContext";
import type { HydratedComponentReference } from "@/utils/componentSpec";

import { DialogContext } from "../Dialogs/dialog.context";
import { PublishedComponentDetails } from "./PublishedComponentDetails";

const mocks = vi.hoisted(() => ({
closeDialog: vi.fn(),
forceUpdateTasks: vi.fn(),
openUpgradeComponentsWindow: vi.fn(),
}));

vi.mock("./hooks/useHasPublishedComponent", () => ({
useHasPublishedComponent: () => ({ data: true }),
}));

vi.mock("./hooks/useOutdatedComponents", () => ({
useOutdatedComponents: (components: HydratedComponentReference[]) => ({
data: components.map((component) => [
component,
{ ...component, digest: "newer-digest" },
]),
}),
}));

vi.mock("./hooks/useForceUpdateTasks", () => ({
useForceUpdateTasks: () => mocks.forceUpdateTasks,
}));

vi.mock("./ComponentUsageCount", () => ({
ComponentUsageCount: () => null,
}));

vi.mock(
"@/routes/v2/pages/Editor/components/UpgradeComponents/useUpgradeComponentsWindow",
() => ({
useUpgradeComponentsWindow: () => mocks.openUpgradeComponentsWindow,
}),
);

const outdatedComponent: HydratedComponentReference = {
digest: "outdated-digest",
name: "Train model",
spec: {
name: "Train model",
implementation: { container: { image: "python:3.12" } },
},
text: "name: Train model",
};

function DialogWrapper({ children }: { children: ReactNode }) {
return (
<DialogContext.Provider
value={{ name: "component-details", close: mocks.closeDialog }}
>
{children}
</DialogContext.Provider>
);
}

function NewEditorDialogWrapper({ children }: { children: ReactNode }) {
return (
<SharedStoreProvider>
<DialogWrapper>{children}</DialogWrapper>
</SharedStoreProvider>
);
}

describe("PublishedComponentDetails", () => {
beforeEach(() => {
vi.clearAllMocks();
});

it("opens the Upgrade Components window from a dialog outside any editor window", () => {
render(<PublishedComponentDetails component={outdatedComponent} />, {
wrapper: NewEditorDialogWrapper,
});

fireEvent.click(screen.getByRole("button", { name: "Review tasks" }));

expect(mocks.closeDialog).toHaveBeenCalledTimes(1);
expect(mocks.openUpgradeComponentsWindow).toHaveBeenCalledTimes(1);
expect(mocks.forceUpdateTasks).not.toHaveBeenCalled();
});

it("reviews tasks on the canvas overlay outside the new editor", () => {
render(<PublishedComponentDetails component={outdatedComponent} />, {
wrapper: DialogWrapper,
});

fireEvent.click(screen.getByRole("button", { name: "Review tasks" }));

expect(mocks.forceUpdateTasks).toHaveBeenCalledWith("outdated-digest");
expect(mocks.openUpgradeComponentsWindow).not.toHaveBeenCalled();
});

it("hides Review tasks when read-only", () => {
render(
<PublishedComponentDetails component={outdatedComponent} readOnly />,
{ wrapper: NewEditorDialogWrapper },
);

expect(
screen.queryByRole("button", { name: "Review tasks" }),
).not.toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ import { Icon } from "@/components/ui/icon";
import { BlockStack, InlineStack } from "@/components/ui/layout";
import { Spinner } from "@/components/ui/spinner";
import { Heading, Paragraph, Text } from "@/components/ui/typography";
import { useUpgradeComponentsWindow } from "@/routes/v2/pages/Editor/components/UpgradeComponents/useUpgradeComponentsWindow";
import { useOptionalSharedStores } from "@/routes/v2/shared/store/SharedStoreContext";
import type { HydratedComponentReference } from "@/utils/componentSpec";

import { useDialogContext } from "../Dialogs/dialog.context";
import { withSuspenseWrapper } from "../SuspenseWrapper";
import { ComponentUsageCount } from "./ComponentUsageCount";
import { useForceUpdateTasks } from "./hooks/useForceUpdateTasks";
Expand All @@ -28,6 +31,38 @@ function PublishedComponentDetailsSkeleton() {
);
}

function ReviewTasksButton({ onLegacyReview }: { onLegacyReview: () => void }) {
const sharedStores = useOptionalSharedStores();

if (sharedStores) {
return <ReviewTasksV2Button />;
}

return (
<Button variant="secondary" size="xs" onClick={onLegacyReview}>
Review tasks
</Button>
);
}

function ReviewTasksV2Button() {
const dialogContext = useDialogContext();
const openUpgradeComponentsWindow = useUpgradeComponentsWindow();

return (
<Button
variant="secondary"
size="xs"
onClick={() => {
dialogContext?.close();
openUpgradeComponentsWindow();
}}
>
Review tasks
</Button>
);
}

function PublishedComponentDetailsContent({
component,
readOnly,
Expand Down Expand Up @@ -86,9 +121,7 @@ function PublishedComponentDetailsContent({
There is a newer version of this component available.
</Paragraph>
{!readOnly && (
<Button variant="secondary" size="xs" onClick={onUpdateTasks}>
Review tasks
</Button>
<ReviewTasksButton onLegacyReview={onUpdateTasks} />
)}
</InlineStack>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1158,6 +1158,8 @@ describe("validateSpec", () => {
expect(issue?.severity).toBe("error");
expect(issue?.entityId).toBe("t1");
expect(issue?.message).toContain("Greet");
expect(issue?.message).toContain("component search");
expect(issue?.message).not.toContain("search_components");
});

it("says nothing when the component has a container", () => {
Expand Down
2 changes: 1 addition & 1 deletion src/models/componentSpec/validation/validateSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ function validateSingleTask(
message:
'Component "' +
(task.resolvedComponentSpec?.name ?? task.name) +
'" has no implementation, so this task cannot run. Give it a container implementation (image and command), or replace it with a component from search_components.',
'" has no implementation, so this task cannot run. Give it a container implementation (image and command), or replace it with a component from component search.',
entityId: task.$id,
severity: "error",
issueCode: "MISSING_IMPLEMENTATION",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@ vi.mock(
}),
);

vi.mock(
"@/components/shared/ReactFlow/FlowSidebar/components/UpgradeAvailableAlertBox",
() => ({
UpgradeAvailableAlertBox: () => (
<div data-testid="upgrade-available-alert" />
),
}),
);

vi.mock("@/providers/AnalyticsProvider", () => ({
useAnalytics: () => ({ track: mocks.track }),
}));
Expand Down Expand Up @@ -99,6 +108,12 @@ describe("ComponentSearchV2Content", () => {
expect(anchor).toContainElement(screen.getByLabelText("Search components"));
});

it("shows the upgrades-available alert", () => {
render(<ComponentSearchV2Content />);

expect(screen.getByTestId("upgrade-available-alert")).toBeInTheDocument();
});

it("shows active AI rerank progress below the search box", async () => {
mocks.useComponentSearchV2State.mockImplementation(() => ({
results: [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
} from "react";

import ImportComponent from "@/components/shared/ReactFlow/FlowSidebar/components/ImportComponent";
import { UpgradeAvailableAlertBox } from "@/components/shared/ReactFlow/FlowSidebar/components/UpgradeAvailableAlertBox";
import { Button } from "@/components/ui/button";
import { Icon } from "@/components/ui/icon";
import { Input } from "@/components/ui/input";
Expand Down Expand Up @@ -183,6 +184,7 @@ export function ComponentSearchV2Content() {
</InlineStack>
}
/>
<UpgradeAvailableAlertBox />
<InlineStack
gap="2"
blockAlign="center"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { focusModeStore } from "@/routes/v2/shared/hooks/useFocusMode";
import { useIsDetailedView } from "@/routes/v2/shared/hooks/useIsDetailedView";
import { useViewportScaling } from "@/routes/v2/shared/hooks/useViewportScaling";
import { useNodeRegistry } from "@/routes/v2/shared/nodes/NodeRegistryContext";
import { CMDALT, SHIFT } from "@/routes/v2/shared/shortcuts/keys";
import { CMD_OR_CTRL, SHIFT } from "@/routes/v2/shared/shortcuts/keys";
import { useSharedStores } from "@/routes/v2/shared/store/SharedStoreContext";

import { CanvasUndoRedo } from "./components/CanvasUndoRedo";
Expand Down Expand Up @@ -52,7 +52,7 @@ export const FlowCanvas = observer(function FlowCanvas({
useState<ReactFlowInstance | null>(null);
const focusModeActive = focusModeStore.active;

const metaKeyPressed = keyboard.pressed.has(CMDALT);
const metaKeyPressed = keyboard.pressed.has(CMD_OR_CTRL);
const shiftKeyPressed = keyboard.pressed.has(SHIFT);
const isConnecting = useConnection((c) => c.inProgress);
const isDetailedView = useIsDetailedView();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
} from "@/routes/v2/shared/clipboard/clipboardMessages";
import { getEffectiveSelection } from "@/routes/v2/shared/clipboard/getEffectiveSelection";
import { useNodeRegistry } from "@/routes/v2/shared/nodes/NodeRegistryContext";
import { CMDALT } from "@/routes/v2/shared/shortcuts/keys";
import { CMD_OR_CTRL } from "@/routes/v2/shared/shortcuts/keys";
import {
hasTextSelection,
isEditableTarget,
Expand All @@ -38,7 +38,7 @@ export function useClipboardShortcuts(
useEffect(() => {
const unregisterDuplicate = keyboard.registerShortcut({
id: "duplicate",
keys: [CMDALT, "D"],
keys: [CMD_OR_CTRL, "D"],
label: "Duplicate",
action: (e) => {
e.preventDefault();
Expand All @@ -51,7 +51,7 @@ export function useClipboardShortcuts(

const unregisterCopy = keyboard.registerShortcut({
id: "copy",
keys: [CMDALT, "C"],
keys: [CMD_OR_CTRL, "C"],
label: "Copy",
action: () => {
if (hasTextSelection()) return false;
Expand All @@ -67,7 +67,7 @@ export function useClipboardShortcuts(

const unregisterPaste = keyboard.registerShortcut({
id: "paste",
keys: [CMDALT, "V"],
keys: [CMD_OR_CTRL, "V"],
label: "Paste",
// Returning false skips preventDefault, so the browser still fires the
// `paste` event the listener below depends on.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
} from "@/routes/v2/pages/Editor/store/actions/aggregator.actions";
import { useEditorSession } from "@/routes/v2/pages/Editor/store/EditorSessionContext";
import type { UndoGroupable } from "@/routes/v2/shared/nodes/types";
import { CMDALT } from "@/routes/v2/shared/shortcuts/keys";
import { CMD_OR_CTRL } from "@/routes/v2/shared/shortcuts/keys";
import { useSharedStores } from "@/routes/v2/shared/store/SharedStoreContext";
import { AGGREGATOR_ADD_INPUT_HANDLE_ID } from "@/utils/aggregatorInputs";

Expand Down Expand Up @@ -139,7 +139,7 @@ export function useConnectionBehavior(
connectionState: FinalConnectionState,
) => {
if (!spec || !reactFlowInstance) return;
if (!keyboard.pressed.has(CMDALT)) return;
if (!keyboard.pressed.has(CMD_OR_CTRL)) return;

const validated = validateConnectEndPreconditions(event, connectionState);
if (!validated) return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ it("copies the selected task only while the view is active", async () => {
const node = canvas.nodes![0];
act(() => canvas.onSelectionChange?.({ nodes: [node], edges: [] }));

fireEvent.keyDown(window, { key: "c", metaKey: true });
fireEvent.keyDown(window, { key: "c", ctrlKey: true });
expect(writeText).toHaveBeenCalledTimes(1);
expect(JSON.parse(writeText.mock.calls[0][0])).toMatchObject({
_type: "tangle-pipeline-nodes",
Expand All @@ -274,15 +274,15 @@ it("copies the selected task only while the view is active", async () => {
});

rerender(<RemotePipelineView spec={spec} isActive={false} />);
fireEvent.keyDown(window, { key: "c", metaKey: true });
fireEvent.keyDown(window, { key: "c", ctrlKey: true });
expect(writeText).toHaveBeenCalledTimes(1);

rerender(<RemotePipelineView spec={spec} isActive />);
fireEvent.keyDown(window, { key: "c", metaKey: true });
fireEvent.keyDown(window, { key: "c", ctrlKey: true });
expect(writeText).toHaveBeenCalledTimes(2);

unmount();
fireEvent.keyDown(window, { key: "c", metaKey: true });
fireEvent.keyDown(window, { key: "c", ctrlKey: true });
expect(writeText).toHaveBeenCalledTimes(2);
});

Expand Down
8 changes: 4 additions & 4 deletions src/routes/v2/pages/Editor/hooks/useUndoRedoKeyboard.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useEffect } from "react";

import { useEditorSession } from "@/routes/v2/pages/Editor/store/EditorSessionContext";
import { CMDALT, SHIFT } from "@/routes/v2/shared/shortcuts/keys";
import { CMD_OR_CTRL, SHIFT } from "@/routes/v2/shared/shortcuts/keys";
import { useSharedStores } from "@/routes/v2/shared/store/SharedStoreContext";

export function useUndoRedoKeyboard(): void {
Expand All @@ -11,21 +11,21 @@ export function useUndoRedoKeyboard(): void {
useEffect(() => {
const unregisterUndo = keyboard.registerShortcut({
id: "undo",
keys: [CMDALT, "Z"],
keys: [CMD_OR_CTRL, "Z"],
label: "Undo",
action: () => undo.undo(),
});

const unregisterRedo = keyboard.registerShortcut({
id: "redo",
keys: [CMDALT, SHIFT, "Z"],
keys: [CMD_OR_CTRL, SHIFT, "Z"],
label: "Redo",
action: () => undo.redo(),
});

const unregisterRedoY = keyboard.registerShortcut({
id: "redo-y",
keys: [CMDALT, "Y"],
keys: [CMD_OR_CTRL, "Y"],
label: "Redo",
action: () => undo.redo(),
});
Expand Down
Loading
Loading