feat: improve notes design (#3938)

* fix notes style

* fix notes style

* adding ghost node

* [autofix.ci] apply automated fixes

* change cursor position

* update notes related test

* [autofix.ci] apply automated fixes

* adjust shadow block width

* move cursor to middle:

* [autofix.ci] apply automated fixes

* fix padding

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Gabriel Luiz Freitas Almeida <gabriel@langflow.org>
This commit is contained in:
Yuqi Tang 2024-10-02 05:45:19 -07:00 • committed by GitHub
commit e58b27062c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 130 additions and 74 deletions

View file

@ -146,7 +146,7 @@ export default function NodeDescription({
) : (
<Markdown
className={cn(
"markdown prose flex h-full w-full flex-col text-primary word-break-break-word dark:prose-invert",
"markdown prose flex h-full w-full flex-col text-primary word-break-break-word note-node-markdown dark:prose-invert",
mdClassName,
)}
>

View file

@ -9,9 +9,7 @@ import { noteDataType } from "@/types/flow";
import { cn } from "@/utils/utils";
import { useEffect, useMemo, useRef, useState } from "react";
import { NodeResizer, NodeToolbar } from "reactflow";
import IconComponent from "../../components/genericIconComponent";
import NodeDescription from "../GenericNode/components/NodeDescription";
import NodeName from "../GenericNode/components/NodeName";
import NoteToolbarComponent from "./NoteToolbarComponent";
function NoteNode({
data,
@ -28,8 +26,8 @@ function NoteNode({
useEffect(() => {
if (nodeDiv.current) {
setSize({
width: nodeDiv.current.offsetWidth - 43,
height: nodeDiv.current.offsetHeight - 80,
width: nodeDiv.current.offsetWidth - 25,
height: nodeDiv.current.offsetHeight - 25,
});
}
}, []);
@ -51,7 +49,7 @@ function NoteNode({
maxWidth={NOTE_NODE_MAX_WIDTH}
onResize={(_, params) => {
const { width, height } = params;
setSize({ width: width - 43, height: height - 80 });
setSize({ width: width - 25, height: height - 25 });
}}
isVisible={selected}
lineClassName="border-[3px] border-border"
@ -67,25 +65,10 @@ function NoteNode({
}}
ref={nodeDiv}
className={cn(
"flex h-full w-full flex-col gap-3 rounded-md border border-b p-5 transition-all",
"flex h-full w-full flex-col gap-3 border border-b p-3 transition-all",
selected ? "" : "-z-50 shadow-sm",
)}
>
<div className="flex h-fit w-full items-center align-middle">
<div className="flex w-full gap-2">
<div data-testid="note_icon">
<IconComponent name="SquarePen" className="min-w-fit" />
</div>
<div className="w-11/12">
<NodeName
nodeId={data.id}
selected={selected}
display_name={data.node?.display_name || "Note"}
/>
</div>
</div>
</div>
<div
style={{
width: size.width,

View file

@ -910,10 +910,20 @@ export const NOTE_NODE_MAX_HEIGHT = 800;
export const NOTE_NODE_MAX_WIDTH = 600;
export const COLOR_OPTIONS = {
default: "var(--note-default)",
indigo: "var(--note-indigo)",
emerald: "var(--note-emerald)",
amber: "var(--note-amber)",
red: "var(--note-red)",
};
export const SHADOW_COLOR_OPTIONS = {
default: "var(--note-default-opacity)",
indigo: "var(--note-indigo-opacity)",
emerald: "var(--note-emerald-opacity)",
amber: "var(--note-amber-opacity)",
red: "var(--note-red-opacity)",
};
export const maxSizeFilesInBytes = 10 * 1024 * 1024; // 10MB in bytes
export const MAX_TEXT_LENGTH = 99999;

View file

@ -3,6 +3,11 @@ import NoteNode from "@/CustomNodes/NoteNode";
import IconComponent from "@/components/genericIconComponent";
import LoadingComponent from "@/components/loadingComponent";
import ShadTooltip from "@/components/shadTooltipComponent";
import {
NOTE_NODE_MIN_HEIGHT,
NOTE_NODE_MIN_WIDTH,
SHADOW_COLOR_OPTIONS,
} from "@/constants/constants";
import { useGetBuildsQuery } from "@/controllers/API/queries/_builds";
import { track } from "@/customization/utils/analytics";
import useAutoSaveFlow from "@/hooks/flows/use-autosave-flow";
@ -109,6 +114,14 @@ export default function Page({ view }: { view?: boolean }): JSX.Element {
useState<OnSelectionChangeParams | null>(null);
const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId);
const [isAddingNote, setIsAddingNote] = useState(false);
const zoomLevel = reactFlowInstance?.getZoom();
const shadowBoxWidth = NOTE_NODE_MIN_WIDTH * (zoomLevel || 1);
const shadowBoxHeight = NOTE_NODE_MIN_HEIGHT * (zoomLevel || 1);
const shadowBoxBackgroundColor =
SHADOW_COLOR_OPTIONS[Object.keys(SHADOW_COLOR_OPTIONS)[0]];
function handleGroupNode() {
takeSnapshot();
if (validateSelection(lastSelection!, edges).length === 0) {
@ -442,9 +455,58 @@ export default function Page({ view }: { view?: boolean }): JSX.Element {
[],
);
const onPaneClick = useCallback(() => {
setFilterEdge([]);
}, []);
const onPaneClick = useCallback(
(event: React.MouseEvent) => {
setFilterEdge([]);
if (isAddingNote) {
const shadowBox = document.getElementById("shadow-box");
if (shadowBox) {
shadowBox.style.display = "none";
}
const position = reactFlowInstance?.screenToFlowPosition({
x: event.clientX - shadowBoxWidth / 2,
y: event.clientY - shadowBoxHeight / 2,
});
const data = {
node: {
description: "",
display_name: "",
documentation: "",
template: {},
},
type: "note",
};
const newId = getNodeId(data.type);
const newNode: NodeType = {
id: newId,
type: "noteNode",
position: position || { x: 0, y: 0 },
data: {
...data,
id: newId,
},
};
setNodes((nds) => nds.concat(newNode));
setIsAddingNote(false);
}
},
[isAddingNote, setNodes, reactFlowInstance, getNodeId, setFilterEdge],
);
const onPaneMouseMove = useCallback(
(event: React.MouseEvent) => {
if (isAddingNote) {
const shadowBox = document.getElementById("shadow-box");
if (shadowBox) {
shadowBox.style.display = "block";
shadowBox.style.left = `${event.clientX - shadowBoxWidth / 2}px`;
shadowBox.style.top = `${event.clientY - shadowBoxHeight / 2}px`;
}
}
},
[isAddingNote],
);
return (
<div className="h-full w-full" ref={reactFlowWrapper}>
@ -483,6 +545,7 @@ export default function Page({ view }: { view?: boolean }): JSX.Element {
panActivationKeyCode={""}
proOptions={{ hideAttribution: true }}
onPaneClick={onPaneClick}
onPaneMouseMove={onPaneMouseMove}
>
<Background className="" />
{!view && (
@ -490,42 +553,7 @@ export default function Page({ view }: { view?: boolean }): JSX.Element {
<ControlButton
data-testid="add_note"
onClick={() => {
const wrapper = reactFlowWrapper.current!;
const viewport = reactFlowInstance?.getViewport();
const x = wrapper.getBoundingClientRect().width / 2;
const y = wrapper.getBoundingClientRect().height / 2;
const nodePosition =
reactFlowInstance?.screenToFlowPosition({ x, y })!;
const data = {
node: {
description: "",
display_name: "",
documentation: "",
template: {},
},
type: "note",
};
const newId = getNodeId(data.type);
const newNode: NodeType = {
id: newId,
type: "noteNode",
position: { x: 0, y: 0 },
data: {
...data,
id: newId,
},
};
paste(
{ nodes: [newNode], edges: [] },
{
x: nodePosition.x,
y: nodePosition?.y,
paneX: wrapper.getBoundingClientRect().x,
paneY: wrapper.getBoundingClientRect().y,
},
);
setIsAddingNote(true);
}}
className="postion react-flow__controls absolute -top-10"
>
@ -550,6 +578,16 @@ export default function Page({ view }: { view?: boolean }): JSX.Element {
}}
/>
</ReactFlow>
<div
id="shadow-box"
style={{
position: "absolute",
width: `${shadowBoxWidth}px`,
height: `${shadowBoxHeight}px`,
backgroundColor: `${shadowBoxBackgroundColor}`,
pointerEvents: "none",
}}
></div>
</div>
) : (
<div className="flex h-full w-full items-center justify-center">

View file

@ -71,19 +71,33 @@
--status-blue: #2563eb;
--status-gray: #6b7280;
--connection: #555;
--note-default: #f1f5f9;
--note-indigo: #e0e7ff;
--note-emerald: #d1fae5;
--note-amber: #fef3c7;
--note-red: #fee2e2;
--note-default-opacity: #f1f5f980;
--note-indigo-opacity: #312e8180;
--note-emerald-opacity: #064e3b80;
--note-amber-opacity: #78350f80;
--note-red-opacity: #7f1d1d80;
}
.dark {
--note-default: #0f172a;
--note-indigo: #312e81;
--note-emerald: #064e3b;
--note-amber: #78350f;
--note-red: #7f1d1d;
--note-placeholder: 216 12% 84%; /* hsl(216 12% 84%) */
--note-default-opacity: #0f172a80;
--note-indigo-opacity: #312e8180;
--note-emerald-opacity: #064e3b80;
--note-amber-opacity: #78350f80;
--note-red-opacity: #7f1d1d80;
--node-selected: 234 89% 74%;
--background: 224 28% 7.5%; /* hsl(224 10% 7.5%) */
--foreground: 213 31% 80%; /* hsl(213 31% 91%) */

View file

@ -255,6 +255,18 @@ const config = {
outline: "none !important",
outlineOffset: "0px !important",
},
".note-node-markdown": {
lineHeight: "1",
"& ul li::marker": {
color: "black",
},
"& ol li::marker": {
color: "black",
},
"& h1, & h2, & h3, & h4, & h5, & h6, & p, & ul, & ol": {
marginBottom: "0.25rem",
},
},
});
}),
tailwindcssTypography,

View file

@ -30,8 +30,15 @@ test("user should be able to interact with sticky notes", async ({ page }) => {
control = "Meta";
}
const noteText = `
Artificial Intelligence (AI) has rapidly evolved from a speculative concept in science fiction to a transformative force reshaping industries and everyday life. The term AI encompasses a broad range of technologies, from simple algorithms designed to perform specific tasks to complex systems capable of learning and adapting independently. As AI continues to advance, its applications are becoming increasingly diverse, impacting everything from healthcare to finance, entertainment, and beyond.
const randomTitle = Math.random()
.toString(36)
.substring(7)
.padEnd(8, "x")
.substring(0, 8);
const noteText = `# ${randomTitle}
Artificial Intelligence (AI) has rapidly evolved from a speculative concept in science fiction to a transformative force reshaping industries and everyday life. The term AI encompasses a broad range of technologies, from simple algorithms designed to perform specific tasks to complex systems capable of learning and adapting independently. As AI continues to advance, its applications are becoming increasingly diverse, impacting everything from healthcare to finance, entertainment, and beyond.
At its core, AI is about creating systems that can perform tasks that would typically require human intelligence. This includes abilities such as visual perception, speech recognition, decision-making, and even language translation. The development of AI can be traced back to the mid-20th century, when pioneers like Alan Turing began exploring the idea of machines that could think. Turing's famous "Turing Test" proposed a benchmark for AI, where a machine would be considered intelligent if it could engage in a conversation with a human without being detected as a machine.
@ -50,8 +57,6 @@ Despite its many benefits, AI also raises important ethical and societal questio
The future of AI is both exciting and uncertain. As the technology continues to advance, it will undoubtedly bring about profound changes in society. The challenge will be to harness AI's potential for good while addressing the ethical and societal issues that arise. Whether it's through smarter healthcare, more efficient transportation, or enhanced creativity, AI has the potential to reshape the world in ways we are only beginning to imagine. The journey of AI is far from over, and its impact will be felt for generations to come.
`;
const randomTitle = Math.random().toString(36).substring(7);
while (modalCount === 0) {
await page.getByText("New Project", { exact: true }).click();
await page.waitForTimeout(3000);
@ -70,6 +75,7 @@ The future of AI is both exciting and uncertain. As the technology continues to
await page.waitForTimeout(1000);
const targetElement = await page.locator('//*[@id="react-flow-id"]');
await targetElement.click();
await page.mouse.up();
await page.mouse.down();
@ -84,19 +90,12 @@ The future of AI is both exciting and uncertain. As the technology continues to
await page.getByTestId("note_node").click();
await page.getByTestId("title-Note").dblclick();
await page.waitForTimeout(1000);
await page.getByTestId("popover-anchor-input-title-Note").fill(randomTitle);
await page.getByTestId("note_icon").first().dblclick();
await page.locator(".generic-node-desc").last().dblclick();
await page.getByTestId("textarea").fill(noteText);
expect(await page.getByText("2500/2500")).toBeVisible();
await page.getByTestId("note_icon").first().dblclick();
await targetElement.click();
const textMarkdown = await page.locator(".markdown").innerText();
const textLength = textMarkdown.length;
@ -110,7 +109,7 @@ The future of AI is both exciting and uncertain. As the technology continues to
let hasStyles = await element?.evaluate((el) => {
const style = window.getComputedStyle(el);
return style.backgroundColor === "rgb(224, 231, 255)";
return style.backgroundColor === "rgb(241, 245, 249)";
});
expect(hasStyles).toBe(true);