Spaces:
Running
Running
Save workspaces.
Browse files- server/main.py +35 -4
- server/ops.py +0 -2
- web/package-lock.json +15 -15
- web/package.json +1 -1
- web/src/App.svelte +1 -2
- web/src/Directory.svelte +51 -2
- web/src/LynxKiteFlow.svelte +21 -27
- web/src/NodeSearch.svelte +22 -5
- web/src/Workspace.svelte +52 -3
server/main.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1 |
from typing import Optional
|
2 |
import dataclasses
|
3 |
import fastapi
|
|
|
4 |
import pathlib
|
5 |
import pydantic
|
6 |
import traceback
|
@@ -78,11 +79,33 @@ def execute(ws):
|
|
78 |
data.view = output
|
79 |
|
80 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
81 |
@app.post("/api/save")
|
82 |
-
def
|
83 |
-
|
84 |
-
execute(ws)
|
85 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
86 |
return ws
|
87 |
|
88 |
DATA_PATH = pathlib.Path.cwd() / 'data'
|
@@ -99,3 +122,11 @@ def list_dir(path: str):
|
|
99 |
return sorted([
|
100 |
DirectoryEntry(p.relative_to(DATA_PATH), 'directory' if p.is_dir() else 'workspace')
|
101 |
for p in path.iterdir()])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
from typing import Optional
|
2 |
import dataclasses
|
3 |
import fastapi
|
4 |
+
import json
|
5 |
import pathlib
|
6 |
import pydantic
|
7 |
import traceback
|
|
|
79 |
data.view = output
|
80 |
|
81 |
|
82 |
+
class SaveRequest(BaseConfig):
|
83 |
+
path: str
|
84 |
+
ws: Workspace
|
85 |
+
|
86 |
+
def save(req: SaveRequest):
|
87 |
+
path = DATA_PATH / req.path
|
88 |
+
assert path.is_relative_to(DATA_PATH)
|
89 |
+
j = req.ws.model_dump_json(indent=2)
|
90 |
+
with open(path, 'w') as f:
|
91 |
+
f.write(j)
|
92 |
+
|
93 |
@app.post("/api/save")
|
94 |
+
def save_and_execute(req: SaveRequest):
|
95 |
+
save(req)
|
96 |
+
execute(req.ws)
|
97 |
+
save(req)
|
98 |
+
return req.ws
|
99 |
+
|
100 |
+
@app.get("/api/load")
|
101 |
+
def load(path: str):
|
102 |
+
path = DATA_PATH / path
|
103 |
+
assert path.is_relative_to(DATA_PATH)
|
104 |
+
if not path.exists():
|
105 |
+
return Workspace(nodes=[], edges=[])
|
106 |
+
with open(path) as f:
|
107 |
+
j = f.read()
|
108 |
+
ws = Workspace.model_validate_json(j)
|
109 |
return ws
|
110 |
|
111 |
DATA_PATH = pathlib.Path.cwd() / 'data'
|
|
|
122 |
return sorted([
|
123 |
DirectoryEntry(p.relative_to(DATA_PATH), 'directory' if p.is_dir() else 'workspace')
|
124 |
for p in path.iterdir()])
|
125 |
+
|
126 |
+
@app.post("/api/dir/mkdir")
|
127 |
+
def make_dir(req: dict):
|
128 |
+
path = DATA_PATH / req['path']
|
129 |
+
assert path.is_relative_to(DATA_PATH)
|
130 |
+
assert not path.exists()
|
131 |
+
path.mkdir()
|
132 |
+
return list_dir(path.parent)
|
server/ops.py
CHANGED
@@ -52,9 +52,7 @@ class Bundle:
|
|
52 |
@classmethod
|
53 |
def from_nx(cls, graph: nx.Graph):
|
54 |
edges = nx.to_pandas_edgelist(graph)
|
55 |
-
print(edges)
|
56 |
nodes = pd.DataFrame({'id': list(graph.nodes)})
|
57 |
-
print(nodes)
|
58 |
return cls(
|
59 |
dfs={'edges': edges, 'nodes': nodes},
|
60 |
edges=[
|
|
|
52 |
@classmethod
|
53 |
def from_nx(cls, graph: nx.Graph):
|
54 |
edges = nx.to_pandas_edgelist(graph)
|
|
|
55 |
nodes = pd.DataFrame({'id': list(graph.nodes)})
|
|
|
56 |
return cls(
|
57 |
dfs={'edges': edges, 'nodes': nodes},
|
58 |
edges=[
|
web/package-lock.json
CHANGED
@@ -8,7 +8,7 @@
|
|
8 |
"name": "vite-svelte-flow-template",
|
9 |
"version": "0.0.0",
|
10 |
"dependencies": {
|
11 |
-
"@xyflow/svelte": "^0.0.
|
12 |
"fuse.js": "^7.0.0",
|
13 |
"graphology": "^0.25.4",
|
14 |
"graphology-library": "^0.8.0",
|
@@ -612,12 +612,12 @@
|
|
612 |
}
|
613 |
},
|
614 |
"node_modules/@xyflow/svelte": {
|
615 |
-
"version": "0.0.
|
616 |
-
"resolved": "https://registry.npmjs.org/@xyflow/svelte/-/svelte-0.0.
|
617 |
-
"integrity": "sha512-
|
618 |
"dependencies": {
|
619 |
"@svelte-put/shortcut": "^3.1.0",
|
620 |
-
"@xyflow/system": "0.0.
|
621 |
"classcat": "^5.0.4",
|
622 |
"svelte-preprocess": "^5.1.3"
|
623 |
},
|
@@ -626,9 +626,9 @@
|
|
626 |
}
|
627 |
},
|
628 |
"node_modules/@xyflow/system": {
|
629 |
-
"version": "0.0.
|
630 |
-
"resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.
|
631 |
-
"integrity": "sha512-
|
632 |
"dependencies": {
|
633 |
"@types/d3": "^7.4.0",
|
634 |
"@types/d3-drag": "^3.0.1",
|
@@ -3107,20 +3107,20 @@
|
|
3107 |
"integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw=="
|
3108 |
},
|
3109 |
"@xyflow/svelte": {
|
3110 |
-
"version": "0.0.
|
3111 |
-
"resolved": "https://registry.npmjs.org/@xyflow/svelte/-/svelte-0.0.
|
3112 |
-
"integrity": "sha512-
|
3113 |
"requires": {
|
3114 |
"@svelte-put/shortcut": "^3.1.0",
|
3115 |
-
"@xyflow/system": "0.0.
|
3116 |
"classcat": "^5.0.4",
|
3117 |
"svelte-preprocess": "^5.1.3"
|
3118 |
}
|
3119 |
},
|
3120 |
"@xyflow/system": {
|
3121 |
-
"version": "0.0.
|
3122 |
-
"resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.
|
3123 |
-
"integrity": "sha512-
|
3124 |
"requires": {
|
3125 |
"@types/d3": "^7.4.0",
|
3126 |
"@types/d3-drag": "^3.0.1",
|
|
|
8 |
"name": "vite-svelte-flow-template",
|
9 |
"version": "0.0.0",
|
10 |
"dependencies": {
|
11 |
+
"@xyflow/svelte": "^0.0.41",
|
12 |
"fuse.js": "^7.0.0",
|
13 |
"graphology": "^0.25.4",
|
14 |
"graphology-library": "^0.8.0",
|
|
|
612 |
}
|
613 |
},
|
614 |
"node_modules/@xyflow/svelte": {
|
615 |
+
"version": "0.0.41",
|
616 |
+
"resolved": "https://registry.npmjs.org/@xyflow/svelte/-/svelte-0.0.41.tgz",
|
617 |
+
"integrity": "sha512-6YE8XJVebBRJnio7y6zgfN/L+J65Tw8F21TGj95c4kxMRVcU1cNRbGT/CJJ4xk3g4bSaxStlblgS/Z2I7mlUHA==",
|
618 |
"dependencies": {
|
619 |
"@svelte-put/shortcut": "^3.1.0",
|
620 |
+
"@xyflow/system": "0.0.21",
|
621 |
"classcat": "^5.0.4",
|
622 |
"svelte-preprocess": "^5.1.3"
|
623 |
},
|
|
|
626 |
}
|
627 |
},
|
628 |
"node_modules/@xyflow/system": {
|
629 |
+
"version": "0.0.21",
|
630 |
+
"resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.21.tgz",
|
631 |
+
"integrity": "sha512-IvvJkC495u8mIA4Xm35dnQp0a5JUwzRm8eDBWKNyI3lAw93dOr85cKSrCNSuQ5M5SWNy2teFCFvnQEgVjwK3dg==",
|
632 |
"dependencies": {
|
633 |
"@types/d3": "^7.4.0",
|
634 |
"@types/d3-drag": "^3.0.1",
|
|
|
3107 |
"integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw=="
|
3108 |
},
|
3109 |
"@xyflow/svelte": {
|
3110 |
+
"version": "0.0.41",
|
3111 |
+
"resolved": "https://registry.npmjs.org/@xyflow/svelte/-/svelte-0.0.41.tgz",
|
3112 |
+
"integrity": "sha512-6YE8XJVebBRJnio7y6zgfN/L+J65Tw8F21TGj95c4kxMRVcU1cNRbGT/CJJ4xk3g4bSaxStlblgS/Z2I7mlUHA==",
|
3113 |
"requires": {
|
3114 |
"@svelte-put/shortcut": "^3.1.0",
|
3115 |
+
"@xyflow/system": "0.0.21",
|
3116 |
"classcat": "^5.0.4",
|
3117 |
"svelte-preprocess": "^5.1.3"
|
3118 |
}
|
3119 |
},
|
3120 |
"@xyflow/system": {
|
3121 |
+
"version": "0.0.21",
|
3122 |
+
"resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.21.tgz",
|
3123 |
+
"integrity": "sha512-IvvJkC495u8mIA4Xm35dnQp0a5JUwzRm8eDBWKNyI3lAw93dOr85cKSrCNSuQ5M5SWNy2teFCFvnQEgVjwK3dg==",
|
3124 |
"requires": {
|
3125 |
"@types/d3": "^7.4.0",
|
3126 |
"@types/d3-drag": "^3.0.1",
|
web/package.json
CHANGED
@@ -21,7 +21,7 @@
|
|
21 |
"vite": "^5.2.8"
|
22 |
},
|
23 |
"dependencies": {
|
24 |
-
"@xyflow/svelte": "^0.0.
|
25 |
"fuse.js": "^7.0.0",
|
26 |
"graphology": "^0.25.4",
|
27 |
"graphology-library": "^0.8.0",
|
|
|
21 |
"vite": "^5.2.8"
|
22 |
},
|
23 |
"dependencies": {
|
24 |
+
"@xyflow/svelte": "^0.0.41",
|
25 |
"fuse.js": "^7.0.0",
|
26 |
"graphology": "^0.25.4",
|
27 |
"graphology-library": "^0.8.0",
|
web/src/App.svelte
CHANGED
@@ -10,8 +10,7 @@
|
|
10 |
if (parts.length > 1) {
|
11 |
parameters = Object.fromEntries(new URLSearchParams(parts[1]));
|
12 |
}
|
13 |
-
|
14 |
-
}
|
15 |
onHashChange();
|
16 |
</script>
|
17 |
|
|
|
10 |
if (parts.length > 1) {
|
11 |
parameters = Object.fromEntries(new URLSearchParams(parts[1]));
|
12 |
}
|
13 |
+
}
|
|
|
14 |
onHashChange();
|
15 |
</script>
|
16 |
|
web/src/Directory.svelte
CHANGED
@@ -3,7 +3,9 @@
|
|
3 |
import logo from './assets/logo.png';
|
4 |
import Home from 'virtual:icons/tabler/home'
|
5 |
import Folder from 'virtual:icons/tabler/folder'
|
|
|
6 |
import File from 'virtual:icons/tabler/file'
|
|
|
7 |
|
8 |
export let path = '';
|
9 |
async function fetchList(path) {
|
@@ -23,6 +25,30 @@
|
|
23 |
function shortName(item) {
|
24 |
return item.name.split('/').pop();
|
25 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
26 |
</script>
|
27 |
|
28 |
<div class="directory-page">
|
@@ -31,10 +57,14 @@
|
|
31 |
<div class="tagline">The Complete Graph Data Science Platform</div>
|
32 |
</div>
|
33 |
<div class="entry-list">
|
34 |
-
{#if path} <div class="breadcrumbs"><a href="#dir"><Home /></a> {path} </div> {/if}
|
35 |
{#await list}
|
36 |
<div>Loading...</div>
|
37 |
{:then list}
|
|
|
|
|
|
|
|
|
|
|
38 |
{#each list as item}
|
39 |
<a class="entry" href={link(item)}>
|
40 |
{#if item.type === 'directory'}
|
@@ -58,7 +88,7 @@
|
|
58 |
background-color: white;
|
59 |
border-radius: 10px;
|
60 |
box-shadow: 0px 2px 4px;
|
61 |
-
padding: 10px 0;
|
62 |
}
|
63 |
@media (min-width: 768px) {
|
64 |
.entry-list {
|
@@ -90,9 +120,27 @@
|
|
90 |
}
|
91 |
}
|
92 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
93 |
.breadcrumbs {
|
94 |
padding-left: 10px;
|
95 |
font-size: 20px;
|
|
|
|
|
|
|
|
|
96 |
}
|
97 |
.entry-list .entry {
|
98 |
display: block;
|
@@ -118,5 +166,6 @@
|
|
118 |
}
|
119 |
a {
|
120 |
color: black;
|
|
|
121 |
}
|
122 |
</style>
|
|
|
3 |
import logo from './assets/logo.png';
|
4 |
import Home from 'virtual:icons/tabler/home'
|
5 |
import Folder from 'virtual:icons/tabler/folder'
|
6 |
+
import FolderPlus from 'virtual:icons/tabler/folder-plus'
|
7 |
import File from 'virtual:icons/tabler/file'
|
8 |
+
import FilePlus from 'virtual:icons/tabler/file-plus'
|
9 |
|
10 |
export let path = '';
|
11 |
async function fetchList(path) {
|
|
|
25 |
function shortName(item) {
|
26 |
return item.name.split('/').pop();
|
27 |
}
|
28 |
+
function newName(list) {
|
29 |
+
let i = 0;
|
30 |
+
while (true) {
|
31 |
+
const name = `Untitled${i ? ` ${i}` : ''}`;
|
32 |
+
if (!list.find(item => item.name === name)) {
|
33 |
+
return name;
|
34 |
+
}
|
35 |
+
i++;
|
36 |
+
}
|
37 |
+
}
|
38 |
+
function newWorkspaceIn(path, list) {
|
39 |
+
const pathSlash = path ? `${path}/` : '';
|
40 |
+
return `#edit?path=${pathSlash}${newName(list)}`;
|
41 |
+
}
|
42 |
+
async function newFolderIn(path, list) {
|
43 |
+
const pathSlash = path ? `${path}/` : '';
|
44 |
+
const name = newName(list);
|
45 |
+
const res = await fetch(`/api/dir/mkdir`, {
|
46 |
+
method: 'POST',
|
47 |
+
headers: { 'Content-Type': 'application/json' },
|
48 |
+
body: JSON.stringify({ path: pathSlash + name }),
|
49 |
+
});
|
50 |
+
list = await res.json();
|
51 |
+
}
|
52 |
</script>
|
53 |
|
54 |
<div class="directory-page">
|
|
|
57 |
<div class="tagline">The Complete Graph Data Science Platform</div>
|
58 |
</div>
|
59 |
<div class="entry-list">
|
|
|
60 |
{#await list}
|
61 |
<div>Loading...</div>
|
62 |
{:then list}
|
63 |
+
<div class="actions">
|
64 |
+
<a href="{newWorkspaceIn(path, list)}"><FilePlus /> New workspace</a>
|
65 |
+
<a href on:click="{newFolderIn(path, list)}"><FolderPlus /> New folder</a>
|
66 |
+
</div>
|
67 |
+
{#if path} <div class="breadcrumbs"><a href="#dir"><Home /></a> {path} </div> {/if}
|
68 |
{#each list as item}
|
69 |
<a class="entry" href={link(item)}>
|
70 |
{#if item.type === 'directory'}
|
|
|
88 |
background-color: white;
|
89 |
border-radius: 10px;
|
90 |
box-shadow: 0px 2px 4px;
|
91 |
+
padding: 0 0 10px 0;
|
92 |
}
|
93 |
@media (min-width: 768px) {
|
94 |
.entry-list {
|
|
|
120 |
}
|
121 |
}
|
122 |
|
123 |
+
.actions {
|
124 |
+
display: flex;
|
125 |
+
justify-content: space-evenly;
|
126 |
+
padding: 5px;
|
127 |
+
}
|
128 |
+
.actions a {
|
129 |
+
padding: 2px 10px;
|
130 |
+
border-radius: 5px;
|
131 |
+
}
|
132 |
+
.actions a:hover {
|
133 |
+
background: #39bcf3;
|
134 |
+
color: white;
|
135 |
+
}
|
136 |
+
|
137 |
.breadcrumbs {
|
138 |
padding-left: 10px;
|
139 |
font-size: 20px;
|
140 |
+
background: #002a4c20;
|
141 |
+
}
|
142 |
+
.breadcrumbs a:hover {
|
143 |
+
color: #39bcf3;
|
144 |
}
|
145 |
.entry-list .entry {
|
146 |
display: block;
|
|
|
166 |
}
|
167 |
a {
|
168 |
color: black;
|
169 |
+
text-decoration: none;
|
170 |
}
|
171 |
</style>
|
web/src/LynxKiteFlow.svelte
CHANGED
@@ -26,22 +26,20 @@
|
|
26 |
table_view: NodeWithTableView,
|
27 |
};
|
28 |
|
|
|
29 |
const nodes = writable<Node[]>([]);
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
// markerEnd: { type: MarkerType.ArrowClosed },
|
43 |
-
},
|
44 |
-
]);
|
45 |
|
46 |
function closeNodeSearch() {
|
47 |
nodeSearchPos = undefined;
|
@@ -52,17 +50,9 @@
|
|
52 |
return;
|
53 |
}
|
54 |
event.preventDefault();
|
55 |
-
const width = 500;
|
56 |
-
const height = 200;
|
57 |
nodeSearchPos = {
|
58 |
-
top: event.
|
59 |
-
left: event.
|
60 |
-
right: event.clientX >= width - 200 ? width - event.clientX : undefined,
|
61 |
-
bottom: event.clientY >= height - 200 ? height - event.clientY : undefined
|
62 |
-
};
|
63 |
-
nodeSearchPos = {
|
64 |
-
top: event.clientY,
|
65 |
-
left: event.clientX - 150,
|
66 |
};
|
67 |
}
|
68 |
function addNode(e) {
|
@@ -99,6 +89,9 @@
|
|
99 |
return JSON.stringify(obj, Array.from(allKeys).sort());
|
100 |
}
|
101 |
graph.subscribe(async (g) => {
|
|
|
|
|
|
|
102 |
const dragging = g.nodes.find((n) => n.dragging);
|
103 |
if (dragging) return;
|
104 |
g = JSON.parse(JSON.stringify(g));
|
@@ -107,13 +100,14 @@
|
|
107 |
}
|
108 |
const ws = orderedJSON(g);
|
109 |
if (ws === backendWorkspace) return;
|
|
|
110 |
backendWorkspace = ws;
|
111 |
const res = await fetch('/api/save', {
|
112 |
method: 'POST',
|
113 |
headers: {
|
114 |
'Content-Type': 'application/json',
|
115 |
},
|
116 |
-
body: ws,
|
117 |
});
|
118 |
const j = await res.json();
|
119 |
backendWorkspace = orderedJSON(j);
|
@@ -121,7 +115,7 @@
|
|
121 |
});
|
122 |
</script>
|
123 |
|
124 |
-
<div style:height="
|
125 |
<SvelteFlow {nodes} {edges} {nodeTypes} fitView
|
126 |
on:paneclick={toggleNodeSearch}
|
127 |
proOptions={{ hideAttribution: true }}
|
|
|
26 |
table_view: NodeWithTableView,
|
27 |
};
|
28 |
|
29 |
+
export let path = '';
|
30 |
const nodes = writable<Node[]>([]);
|
31 |
+
const edges = writable<Edge[]>([]);
|
32 |
+
let workspaceLoaded = false;
|
33 |
+
async function fetchWorkspace(path) {
|
34 |
+
if (!path) return;
|
35 |
+
const res = await fetch(`/api/load?path=${path}`);
|
36 |
+
const j = await res.json();
|
37 |
+
nodes.set(j.nodes);
|
38 |
+
edges.set(j.edges);
|
39 |
+
backendWorkspace = orderedJSON(j);
|
40 |
+
workspaceLoaded = true;
|
41 |
+
}
|
42 |
+
$: fetchWorkspace(path);
|
|
|
|
|
|
|
43 |
|
44 |
function closeNodeSearch() {
|
45 |
nodeSearchPos = undefined;
|
|
|
50 |
return;
|
51 |
}
|
52 |
event.preventDefault();
|
|
|
|
|
53 |
nodeSearchPos = {
|
54 |
+
top: event.offsetY,
|
55 |
+
left: event.offsetX - 155,
|
|
|
|
|
|
|
|
|
|
|
|
|
56 |
};
|
57 |
}
|
58 |
function addNode(e) {
|
|
|
89 |
return JSON.stringify(obj, Array.from(allKeys).sort());
|
90 |
}
|
91 |
graph.subscribe(async (g) => {
|
92 |
+
if (!workspaceLoaded) {
|
93 |
+
return;
|
94 |
+
}
|
95 |
const dragging = g.nodes.find((n) => n.dragging);
|
96 |
if (dragging) return;
|
97 |
g = JSON.parse(JSON.stringify(g));
|
|
|
100 |
}
|
101 |
const ws = orderedJSON(g);
|
102 |
if (ws === backendWorkspace) return;
|
103 |
+
console.log('save', '\n' + ws, '\n' + backendWorkspace);
|
104 |
backendWorkspace = ws;
|
105 |
const res = await fetch('/api/save', {
|
106 |
method: 'POST',
|
107 |
headers: {
|
108 |
'Content-Type': 'application/json',
|
109 |
},
|
110 |
+
body: JSON.stringify({ path, ws: g }),
|
111 |
});
|
112 |
const j = await res.json();
|
113 |
backendWorkspace = orderedJSON(j);
|
|
|
115 |
});
|
116 |
</script>
|
117 |
|
118 |
+
<div style:height="100%">
|
119 |
<SvelteFlow {nodes} {edges} {nodeTypes} fitView
|
120 |
on:paneclick={toggleNodeSearch}
|
121 |
proOptions={{ hideAttribution: true }}
|
web/src/NodeSearch.svelte
CHANGED
@@ -23,13 +23,21 @@
|
|
23 |
e.preventDefault();
|
24 |
selectedIndex = Math.max(selectedIndex - 1, 0);
|
25 |
} else if (e.key === 'Enter') {
|
26 |
-
|
27 |
-
node.position = {x: pos.left, y: pos.top};
|
28 |
-
dispatch('add', node);
|
29 |
} else if (e.key === 'Escape') {
|
30 |
dispatch('cancel');
|
31 |
}
|
32 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
33 |
|
34 |
</script>
|
35 |
|
@@ -40,10 +48,18 @@ style="top: {pos.top}px; left: {pos.left}px; right: {pos.right}px; bottom: {pos.
|
|
40 |
bind:this={searchBox}
|
41 |
on:input={onInput}
|
42 |
on:keydown={onKeyDown}
|
43 |
-
on:focusout={
|
44 |
placeholder="Search for box">
|
45 |
{#each hits as box, index}
|
46 |
-
<div
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
47 |
{/each}
|
48 |
</div>
|
49 |
|
@@ -58,6 +74,7 @@ style="top: {pos.top}px; left: {pos.left}px; right: {pos.right}px; bottom: {pos.
|
|
58 |
}
|
59 |
.search-result {
|
60 |
padding: 4px;
|
|
|
61 |
}
|
62 |
.search-result.selected {
|
63 |
background-color: #f80;
|
|
|
23 |
e.preventDefault();
|
24 |
selectedIndex = Math.max(selectedIndex - 1, 0);
|
25 |
} else if (e.key === 'Enter') {
|
26 |
+
addSelected();
|
|
|
|
|
27 |
} else if (e.key === 'Escape') {
|
28 |
dispatch('cancel');
|
29 |
}
|
30 |
}
|
31 |
+
function addSelected() {
|
32 |
+
const node = {...hits[selectedIndex].item};
|
33 |
+
node.position = {x: pos.left, y: pos.top};
|
34 |
+
dispatch('add', node);
|
35 |
+
}
|
36 |
+
async function lostFocus(e) {
|
37 |
+
// If it's a click on a result, let the click handler handle it.
|
38 |
+
if (e.relatedTarget && e.relatedTarget.closest('.node-search')) return;
|
39 |
+
dispatch('cancel');
|
40 |
+
}
|
41 |
|
42 |
</script>
|
43 |
|
|
|
48 |
bind:this={searchBox}
|
49 |
on:input={onInput}
|
50 |
on:keydown={onKeyDown}
|
51 |
+
on:focusout={lostFocus}
|
52 |
placeholder="Search for box">
|
53 |
{#each hits as box, index}
|
54 |
+
<div
|
55 |
+
tabindex="0"
|
56 |
+
on:focus={() => selectedIndex = index}
|
57 |
+
on:mouseenter={() => selectedIndex = index}
|
58 |
+
on:click={addSelected}
|
59 |
+
class="search-result"
|
60 |
+
class:selected={index == selectedIndex}>
|
61 |
+
{box.item.data.title}
|
62 |
+
</div>
|
63 |
{/each}
|
64 |
</div>
|
65 |
|
|
|
74 |
}
|
75 |
.search-result {
|
76 |
padding: 4px;
|
77 |
+
cursor: pointer;
|
78 |
}
|
79 |
.search-result.selected {
|
80 |
background-color: #f80;
|
web/src/Workspace.svelte
CHANGED
@@ -1,9 +1,58 @@
|
|
1 |
<script lang="ts">
|
2 |
// This is the whole LynxKite workspace editor page.
|
3 |
import { SvelteFlowProvider } from '@xyflow/svelte';
|
|
|
|
|
|
|
4 |
import LynxKiteFlow from './LynxKiteFlow.svelte';
|
|
|
|
|
5 |
</script>
|
6 |
|
7 |
-
<
|
8 |
-
<
|
9 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
<script lang="ts">
|
2 |
// This is the whole LynxKite workspace editor page.
|
3 |
import { SvelteFlowProvider } from '@xyflow/svelte';
|
4 |
+
import ArrowBack from 'virtual:icons/tabler/arrow-back'
|
5 |
+
import Backspace from 'virtual:icons/tabler/backspace'
|
6 |
+
import Atom from 'virtual:icons/tabler/Atom'
|
7 |
import LynxKiteFlow from './LynxKiteFlow.svelte';
|
8 |
+
export let path = '';
|
9 |
+
$: parent = path.split('/').slice(0, -1).join('/');
|
10 |
</script>
|
11 |
|
12 |
+
<div class="page">
|
13 |
+
<div class="top-bar">
|
14 |
+
<div class="ws-name">
|
15 |
+
<a href><img src="/favicon.ico"></a>
|
16 |
+
{path}
|
17 |
+
</div>
|
18 |
+
<div class="tools">
|
19 |
+
<a href><Atom /></a>
|
20 |
+
<a href><Backspace /></a>
|
21 |
+
<a href="#dir?path={parent}"><ArrowBack /></a>
|
22 |
+
</div>
|
23 |
+
</div>
|
24 |
+
<SvelteFlowProvider>
|
25 |
+
<LynxKiteFlow path={path} />
|
26 |
+
</SvelteFlowProvider>
|
27 |
+
</div>
|
28 |
+
|
29 |
+
<style>
|
30 |
+
.top-bar {
|
31 |
+
display: flex;
|
32 |
+
justify-content: space-between;
|
33 |
+
background: #002a4c;
|
34 |
+
color: white;
|
35 |
+
}
|
36 |
+
.ws-name {
|
37 |
+
font-size: 1.5em;
|
38 |
+
}
|
39 |
+
.ws-name img {
|
40 |
+
height: 1.5em;
|
41 |
+
vertical-align: middle;
|
42 |
+
margin: 4px;
|
43 |
+
}
|
44 |
+
.page {
|
45 |
+
display: flex;
|
46 |
+
flex-direction: column;
|
47 |
+
height: 100vh;
|
48 |
+
}
|
49 |
+
|
50 |
+
.tools {
|
51 |
+
display: flex;
|
52 |
+
align-items: center;
|
53 |
+
}
|
54 |
+
.tools a {
|
55 |
+
color: white;
|
56 |
+
font-size: 1.5em;
|
57 |
+
}
|
58 |
+
</style>
|