darabos commited on
Commit
7194a89
·
1 Parent(s): db13d53

Automatically find location for "/".

Browse files
lynxkite-app/uv.lock CHANGED
The diff for this file is too large to render. See raw diff
 
lynxkite-app/web/src/apiTypes.ts CHANGED
@@ -34,6 +34,8 @@ export interface WorkspaceNode {
34
  type: string;
35
  data: WorkspaceNodeData;
36
  position: Position;
 
 
37
  [k: string]: unknown;
38
  }
39
  export interface WorkspaceNodeData {
 
34
  type: string;
35
  data: WorkspaceNodeData;
36
  position: Position;
37
+ width: number;
38
+ height: number;
39
  [k: string]: unknown;
40
  }
41
  export interface WorkspaceNodeData {
lynxkite-app/web/src/workspace/Workspace.tsx CHANGED
@@ -16,7 +16,7 @@ import {
16
  useUpdateNodeInternals,
17
  } from "@xyflow/react";
18
  import axios from "axios";
19
- import { type MouseEvent, useCallback, useEffect, useMemo, useState } from "react";
20
  import { Link } from "react-router";
21
  import useSWR, { type Fetcher } from "swr";
22
  import { WebsocketProvider } from "y-websocket";
@@ -61,6 +61,7 @@ export default function Workspace(props: any) {
61
  function LynxKiteFlow() {
62
  const updateNodeInternals = useUpdateNodeInternals();
63
  const reactFlow = useReactFlow();
 
64
  const [nodes, setNodes] = useState([] as Node[]);
65
  const [edges, setEdges] = useState([] as Edge[]);
66
  const path = usePath().replace(/^[/]edit[/]/, "");
@@ -210,7 +211,7 @@ function LynxKiteFlow() {
210
  if (event.key === "/") {
211
  event.preventDefault();
212
  setNodeSearchSettings({
213
- pos: { x: 100, y: 100 },
214
  boxes: catalog.data![state.workspace.env!],
215
  });
216
  } else if (event.key === "r") {
@@ -225,6 +226,39 @@ function LynxKiteFlow() {
225
  };
226
  }, [catalog.data, nodeSearchSettings, state.workspace.env]);
227
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
228
  function isTypingInFormElement() {
229
  const activeElement = document.activeElement;
230
  return (
@@ -504,7 +538,12 @@ function LynxKiteFlow() {
504
  </Tooltip>
505
  </div>
506
  </div>
507
- <div style={{ height: "100%", width: "100vw" }} onDragOver={onDragOver} onDrop={onDrop}>
 
 
 
 
 
508
  <LynxKiteState.Provider value={state}>
509
  <ReactFlow
510
  nodes={nodes}
@@ -537,6 +576,7 @@ function LynxKiteFlow() {
537
  stroke: "black",
538
  },
539
  }}
 
540
  >
541
  <Controls />
542
  {nodeSearchSettings && (
 
16
  useUpdateNodeInternals,
17
  } from "@xyflow/react";
18
  import axios from "axios";
19
+ import { type MouseEvent, useCallback, useEffect, useMemo, useRef, useState } from "react";
20
  import { Link } from "react-router";
21
  import useSWR, { type Fetcher } from "swr";
22
  import { WebsocketProvider } from "y-websocket";
 
61
  function LynxKiteFlow() {
62
  const updateNodeInternals = useUpdateNodeInternals();
63
  const reactFlow = useReactFlow();
64
+ const reactFlowContainer = useRef<HTMLDivElement>(null);
65
  const [nodes, setNodes] = useState([] as Node[]);
66
  const [edges, setEdges] = useState([] as Edge[]);
67
  const path = usePath().replace(/^[/]edit[/]/, "");
 
211
  if (event.key === "/") {
212
  event.preventDefault();
213
  setNodeSearchSettings({
214
+ pos: getBestPosition(),
215
  boxes: catalog.data![state.workspace.env!],
216
  });
217
  } else if (event.key === "r") {
 
226
  };
227
  }, [catalog.data, nodeSearchSettings, state.workspace.env]);
228
 
229
+ function getBestPosition() {
230
+ const W = reactFlowContainer.current!.clientWidth;
231
+ const H = reactFlowContainer.current!.clientHeight;
232
+ const w = 200;
233
+ const h = 200;
234
+ const SPEED = 20;
235
+ const GAP = 50;
236
+ const pos = { x: 100, y: 100 };
237
+ while (pos.y < H) {
238
+ // Find a position that is not occupied by a node.
239
+ const fpos = reactFlow.screenToFlowPosition(pos);
240
+ const occupied = state.workspace.nodes!.some((n) => {
241
+ const np = n.position;
242
+ return (
243
+ np.x < fpos.x + w + GAP &&
244
+ np.x + n.width + GAP > fpos.x &&
245
+ np.y < fpos.y + h + GAP &&
246
+ np.y + n.height + GAP > fpos.y
247
+ );
248
+ });
249
+ if (!occupied) {
250
+ return pos;
251
+ }
252
+ // Move the position to the right and down until we find a free spot.
253
+ pos.x += SPEED;
254
+ if (pos.x + w > W) {
255
+ pos.x = 100;
256
+ pos.y += SPEED;
257
+ }
258
+ }
259
+ return { x: 100, y: 100 };
260
+ }
261
+
262
  function isTypingInFormElement() {
263
  const activeElement = document.activeElement;
264
  return (
 
538
  </Tooltip>
539
  </div>
540
  </div>
541
+ <div
542
+ style={{ height: "100%", width: "100vw" }}
543
+ onDragOver={onDragOver}
544
+ onDrop={onDrop}
545
+ ref={reactFlowContainer}
546
+ >
547
  <LynxKiteState.Provider value={state}>
548
  <ReactFlow
549
  nodes={nodes}
 
576
  stroke: "black",
577
  },
578
  }}
579
+ fitViewOptions={{ maxZoom: 1 }}
580
  >
581
  <Controls />
582
  {nodeSearchSettings && (
lynxkite-app/web/src/workspace/nodes/NodeParameter.tsx CHANGED
@@ -237,7 +237,7 @@ export default function NodeParameter({ name, value, meta, data, setParam }: Nod
237
  <textarea
238
  className="textarea textarea-bordered w-full"
239
  rows={6}
240
- value={value}
241
  onChange={(evt) => onChange(evt.currentTarget.value, { delay: 2 })}
242
  onBlur={(evt) => onChange(evt.currentTarget.value, { delay: 0 })}
243
  />
 
237
  <textarea
238
  className="textarea textarea-bordered w-full"
239
  rows={6}
240
+ value={value || ""}
241
  onChange={(evt) => onChange(evt.currentTarget.value, { delay: 2 })}
242
  onBlur={(evt) => onChange(evt.currentTarget.value, { delay: 0 })}
243
  />
lynxkite-app/web/tests/lynxkite.ts CHANGED
@@ -53,14 +53,8 @@ export class Workspace {
53
  }
54
 
55
  async addBox(boxName) {
56
- //TODO: Support passing box parameters (id, position, etc.)
57
  const allBoxes = await this.getBoxes().all();
58
- if (allBoxes) {
59
- // Avoid overlapping with existing nodes
60
- const numNodes = allBoxes.length || 1;
61
- await this.page.mouse.wheel(0, numNodes * 400);
62
- }
63
-
64
  await this.page.locator(".ws-name").click();
65
  await this.page.keyboard.press("/");
66
  await this.page.locator(".node-search").getByText(boxName, { exact: true }).click();
 
53
  }
54
 
55
  async addBox(boxName) {
56
+ // TODO: Support passing box parameters.
57
  const allBoxes = await this.getBoxes().all();
 
 
 
 
 
 
58
  await this.page.locator(".ws-name").click();
59
  await this.page.keyboard.press("/");
60
  await this.page.locator(".node-search").getByText(boxName, { exact: true }).click();
lynxkite-core/src/lynxkite/core/workspace.py CHANGED
@@ -40,12 +40,13 @@ class WorkspaceNodeData(BaseConfig):
40
 
41
 
42
  class WorkspaceNode(BaseConfig):
43
- # The naming of these attributes matches the ones for the NodeBase type in React flow
44
- # modyfing them will break the frontend.
45
  id: str
46
  type: str
47
  data: WorkspaceNodeData
48
  position: Position
 
 
49
  _crdt: pycrdt.Map
50
 
51
  def publish_started(self):
 
40
 
41
 
42
  class WorkspaceNode(BaseConfig):
43
+ # Most of these fields are shared with ReactFlow.
 
44
  id: str
45
  type: str
46
  data: WorkspaceNodeData
47
  position: Position
48
+ width: float
49
+ height: float
50
  _crdt: pycrdt.Map
51
 
52
  def publish_started(self):