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

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion frontend/e2e-tests/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,13 @@ export async function createCellBelow(opts: {
page.getByTestId("create-cell-button").locator(":visible"),
).toHaveCount(2);

// Clicking the first button creates a new cell below
// Clicking the first button opens a dropdown menu with the cell types
await page
.getByTestId("create-cell-button")
.locator(":visible")
.last()
.click();
await page.getByText("Python cell").click();
// Type into the currently focused cell
if (content) {
await page.locator("*:focus").type(content);
Expand Down
15 changes: 8 additions & 7 deletions frontend/e2e-tests/py/bad_button.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,28 @@
# [tool.marimo.runtime]
# auto_instantiate = true
# ///

import marimo

__generated_with = "0.0.1"
__generated_with = "0.17.6"
app = marimo.App()


@app.cell
def __():
def _():
import marimo as mo
return mo,
return (mo,)


@app.cell
def __(mo):
b = mo.ui.button(value=None, label='Bad button', on_click=lambda v: v + 1)
def _(mo):
b = mo.ui.button(value=None, label="Bad button", on_click=lambda v: v + 1)
b
return b,
return (b,)


@app.cell
def __(b):
def _(b):
b.value
return

Expand Down
186 changes: 98 additions & 88 deletions frontend/src/components/editor/cell/CreateCellButton.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
/* Copyright 2024 Marimo. All rights reserved. */
import { DatabaseIcon, DiamondPlusIcon, PlusIcon } from "lucide-react";
import { useState } from "react";
import { Button } from "@/components/editor/inputs/Inputs";
import { MinimalHotkeys } from "@/components/shortcuts/renderShortcut";
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuTrigger,
} from "@/components/ui/context-menu";
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { maybeAddMarimoImport } from "@/core/cells/add-missing-import";
import { useCellActions } from "@/core/cells/cells";
import { LanguageAdapters } from "@/core/codemirror/language/LanguageAdapters";
Expand All @@ -24,11 +26,17 @@ export const CreateCellButton = ({
connectionState,
onClick,
tooltipContent,
oneClickShortcut,
}: {
connectionState: WebSocketState;
tooltipContent: React.ReactNode;
onClick: ((opts: { code: string; hideCode?: boolean }) => void) | undefined;
oneClickShortcut: "shift" | "mod";
}) => {
const { createNewCell, addSetupCellIfDoesntExist } = useCellActions();
const shortcut = `${oneClickShortcut}-Click`;
const [justOpened, setJustOpened] = useState(false);

const baseTooltipContent =
getConnectionTooltip(connectionState) || tooltipContent;
const finalTooltipContent = isAppInteractionDisabled(connectionState) ? (
Expand All @@ -37,107 +45,109 @@ export const CreateCellButton = ({
<div className="flex flex-col gap-4">
<div>{baseTooltipContent}</div>
<div className="text-xs text-muted-foreground font-medium pt-1 -mt-2 border-t border-border">
Right-click for cell types
{<MinimalHotkeys shortcut={shortcut} className="inline" />}{" "}
<span>to auto insert a cell</span>
</div>
</div>
);

const addPythonCell = () => {
onClick?.({ code: "" });
};

// NB: When adding the marimo import for markdown and SQL, we run it
// automatically regardless of whether autoinstantiate or lazy execution is
// enabled; the user experience is confusing otherwise (how does the user
// know they need to run import marimo as mo. first?).
const addMarkdownCell = () => {
maybeAddMarimoImport({ autoInstantiate: true, createNewCell });
onClick?.({
code: LanguageAdapters.markdown.defaultCode,
hideCode: true,
});
};

const addSQLCell = () => {
maybeAddMarimoImport({ autoInstantiate: true, createNewCell });
onClick?.({ code: LanguageAdapters.sql.defaultCode });
};

const addSetupCell = () => {
addSetupCellIfDoesntExist({});
};

const renderIcon = (icon: React.ReactNode) => {
return <div className="mr-3 text-muted-foreground">{icon}</div>;
};

const handleButtonClick = (e: React.MouseEvent) => {
if (oneClickShortcut === "shift" ? e.shiftKey : e.metaKey || e.ctrlKey) {
e.preventDefault();
e.stopPropagation();
addPythonCell();
}
};

const handleOpenChange = (open: boolean) => {
if (open) {
setJustOpened(true);
// Allow interactions after a brief delay
setTimeout(() => {
setJustOpened(false);
}, 200);
}
};

const handleFirstItemClick = (e: React.MouseEvent) => {
// Hack to prevent the first item from being clicked when the dropdown is opened
if (justOpened) {
e.preventDefault();
e.stopPropagation();
return;
}
addPythonCell();
};

return (
<CreateCellButtonContextMenu onClick={onClick}>
<Tooltip content={finalTooltipContent}>
<DropdownMenu onOpenChange={handleOpenChange}>
<DropdownMenuTrigger asChild={true} onPointerDown={handleButtonClick}>
<Button
onClick={() => onClick?.({ code: "" })}
className={cn(
"shoulder-button hover-action",
"shoulder-button hover-action border-none shadow-none! bg-transparent! focus-visible:outline-none",
isAppInteractionDisabled(connectionState) && " inactive-button",
)}
onMouseDown={Events.preventFocus}
shape="circle"
size="small"
color="hint-green"
data-testid="create-cell-button"
>
<PlusIcon strokeWidth={1.8} />
<Tooltip content={finalTooltipContent}>
<PlusIcon
strokeWidth={4}
size={14}
className="opacity-60 hover:opacity-90"
/>
</Tooltip>
</Button>
</Tooltip>
</CreateCellButtonContextMenu>
);
};

const CreateCellButtonContextMenu = (props: {
onClick: ((opts: { code: string; hideCode?: boolean }) => void) | undefined;
children: React.ReactNode;
}) => {
const { children, onClick } = props;
const { createNewCell, addSetupCellIfDoesntExist } = useCellActions();

if (!onClick) {
return children;
}

// NB: When adding the marimo import for markdown and SQL, we run it
// automatically regardless of whether autoinstantiate or lazy execution is
// enabled; the user experience is confusing otherwise (how does the user
// know they need to run import marimo as mo. first?).
return (
<ContextMenu>
<ContextMenuTrigger>{children}</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem
key="python"
onSelect={(evt) => {
evt.stopPropagation();
onClick({ code: "" });
}}
>
<div className="mr-3 text-muted-foreground">
<PythonIcon />
</div>
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" sideOffset={-30}>
<DropdownMenuItem onClick={handleFirstItemClick}>
{renderIcon(<PythonIcon />)}
Python cell
</ContextMenuItem>

<ContextMenuItem
key="markdown"
onSelect={(evt) => {
evt.stopPropagation();
maybeAddMarimoImport({ autoInstantiate: true, createNewCell });
onClick({
code: LanguageAdapters.markdown.defaultCode,
hideCode: true,
});
}}
>
<div className="mr-3 text-muted-foreground">
<MarkdownIcon />
</div>
</DropdownMenuItem>
<DropdownMenuItem onClick={addMarkdownCell}>
{renderIcon(<MarkdownIcon />)}
Markdown cell
</ContextMenuItem>
<ContextMenuItem
key="sql"
onSelect={(evt) => {
evt.stopPropagation();
maybeAddMarimoImport({ autoInstantiate: true, createNewCell });
onClick({ code: LanguageAdapters.sql.defaultCode });
}}
>
<div className="mr-3 text-muted-foreground">
<DatabaseIcon size={13} strokeWidth={1.5} />
</div>
</DropdownMenuItem>
<DropdownMenuItem onClick={addSQLCell}>
{renderIcon(<DatabaseIcon size={13} strokeWidth={1.5} />)}
SQL cell
</ContextMenuItem>
<ContextMenuItem
key="setup"
onSelect={(evt) => {
evt.stopPropagation();
addSetupCellIfDoesntExist({});
}}
>
<div className="mr-3 text-muted-foreground">
<DiamondPlusIcon size={13} strokeWidth={1.5} />
</div>
</DropdownMenuItem>
<DropdownMenuItem onClick={addSetupCell}>
{renderIcon(<DiamondPlusIcon size={13} strokeWidth={1.5} />)}
Setup cell
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
};
46 changes: 22 additions & 24 deletions frontend/src/components/editor/notebook-cell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -489,7 +489,7 @@ const EditableCellComponent = ({

const outputArea = hasOutput && (
<div className="relative" onDoubleClick={showHiddenCodeIfMarkdown}>
<div className="absolute top-5 -left-8 z-10 print:hidden">
<div className="absolute top-5 -left-7 z-20 print:hidden">
<CollapseToggle
isCollapsed={isCollapsed}
onClick={() => {
Expand Down Expand Up @@ -580,6 +580,7 @@ const EditableCellComponent = ({
ref={cellContainerRef}
{...cellDomProps(cellId, cellData.name)}
>
<CellLeftSideActions cellId={cellId} actions={actions} />
{cellOutput === "above" && outputArea}
<div
className={cn("tray")}
Expand All @@ -602,17 +603,6 @@ const EditableCellComponent = ({
onRun={runCell}
/>
</div>
<CellLeftSideActions
cellId={cellId}
className={cn(
isMarkdownCodeHidden && hasOutputAbove && "-top-7",
isMarkdownCodeHidden && hasOutputBelow && "-bottom-8",
isMarkdownCodeHidden &&
isCellButtonsInline &&
"-left-[3.8rem]",
)}
actions={actions}
/>
<CellEditor
theme={theme}
showPlaceholder={showPlaceholder}
Expand Down Expand Up @@ -840,25 +830,33 @@ const CellLeftSideActions = memo(
);

const isConnected = isAppConnected(connection.state);
const oneClickShortcut = "mod";

return (
<div
className={cn(
"absolute flex flex-col gap-[2px] justify-center h-full left-[-34px] z-20",
"absolute flex flex-col justify-center h-full left-[-26px] z-20 border-b-0!",
className,
)}
>
<CreateCellButton
tooltipContent={renderShortcut("cell.createAbove")}
connectionState={connection.state}
onClick={isConnected ? createAbove : undefined}
/>
<div className="flex-1" />
<CreateCellButton
tooltipContent={renderShortcut("cell.createBelow")}
connectionState={connection.state}
onClick={isConnected ? createBelow : undefined}
/>
<div className="-mt-1">
<CreateCellButton
tooltipContent={renderShortcut("cell.createAbove")}
connectionState={connection.state}
onClick={isConnected ? createAbove : undefined}
oneClickShortcut={oneClickShortcut}
/>
</div>
<div className="flex-1 pointer-events-none w-3" />
{/* <div className="flex-1 pointer-events-none bg-border w-px mx-auto hover-action opacity-70" /> */}
<div className="-mb-2">
<CreateCellButton
tooltipContent={renderShortcut("cell.createBelow")}
connectionState={connection.state}
onClick={isConnected ? createBelow : undefined}
oneClickShortcut={oneClickShortcut}
/>
</div>
</div>
);
},
Expand Down
Loading