vzcode 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -0
- package/dist/assets/index-29b6a606.css +1 -0
- package/dist/assets/{index-4694dca8.js → index-523ecad7.js} +30 -30
- package/dist/index.html +2 -2
- package/package.json +15 -6
- package/src/{App.jsx → client/App.jsx} +31 -81
- package/src/client/Sidebar/Directory.jsx +62 -0
- package/src/client/Sidebar/DirectoryArrowSVG.jsx +5 -0
- package/src/client/Sidebar/File.jsx +32 -0
- package/src/client/Sidebar/FileOrDirectory.jsx +33 -0
- package/src/client/Sidebar/Item.jsx +43 -0
- package/src/client/Sidebar/index.jsx +64 -0
- package/src/client/Sidebar/styles.css +88 -0
- package/src/client/Sidebar/useOpenDirectories.js +39 -0
- package/src/{diff.js → client/diff.js} +1 -1
- package/src/client/featureFlags.js +3 -0
- package/src/client/getFileTree.js +49 -0
- package/src/{getOrCreateEditor.js → client/getOrCreateEditor.js} +1 -2
- package/src/{json1PresenceDisplay.js → client/json1PresenceDisplay.js} +0 -1
- package/src/{settings.jsx → client/settings.jsx} +1 -1
- package/src/client/sortFileTree.js +15 -0
- package/src/{style.css → client/style.css} +0 -87
- package/src/server/computeInitialDocument.js +69 -0
- package/src/server/featureFlags.js +4 -0
- package/{cli.js → src/server/index.js} +25 -29
- package/dist/assets/index-58524f84.css +0 -1
- /package/src/{CodeEditor.jsx → client/CodeEditor.jsx} +0 -0
- /package/src/{json1PresenceBroadcast.js → client/json1PresenceBroadcast.js} +0 -0
- /package/src/{main.jsx → client/main.jsx} +0 -0
package/dist/index.html
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>VZCode</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
-
<link rel="stylesheet" href="/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-523ecad7.js"></script>
|
|
8
|
+
<link rel="stylesheet" href="/assets/index-29b6a606.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
11
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vzcode",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Multiplayer code editor system",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
7
|
-
"bin": "
|
|
7
|
+
"bin": "src/server/index.js",
|
|
8
8
|
"files": [
|
|
9
9
|
"dist/**",
|
|
10
10
|
"src/**"
|
|
11
11
|
],
|
|
12
12
|
"scripts": {
|
|
13
|
-
"test": "
|
|
14
|
-
"
|
|
13
|
+
"test": "vitest",
|
|
14
|
+
"test-interactive": "npm run build; cd test/sampleDirectories/kitchenSink; node ../../../src/server/index.js",
|
|
15
|
+
"prettier": "prettier {*.*,**/*.*} --write",
|
|
15
16
|
"dev": "vite",
|
|
16
17
|
"build": "vite build",
|
|
17
18
|
"build-release": "vite build --mode release",
|
|
@@ -33,7 +34,10 @@
|
|
|
33
34
|
"Data",
|
|
34
35
|
"Visualization"
|
|
35
36
|
],
|
|
36
|
-
"
|
|
37
|
+
"contributors": [
|
|
38
|
+
"Curran Kelleher",
|
|
39
|
+
"Anooj Pai"
|
|
40
|
+
],
|
|
37
41
|
"license": "MIT",
|
|
38
42
|
"bugs": {
|
|
39
43
|
"url": "https://github.com/vizhub-core/vzcode/issues"
|
|
@@ -49,9 +53,13 @@
|
|
|
49
53
|
"@teamwork/websocket-json-stream": "^2.0.0",
|
|
50
54
|
"codemirror": "^6.0.1",
|
|
51
55
|
"codemirror-ot": "^4.0.0",
|
|
56
|
+
"d3-array": "^3.2.3",
|
|
52
57
|
"diff-match-patch": "^1.0.5",
|
|
58
|
+
"dotenv": "^16.0.3",
|
|
53
59
|
"express": "^4.18.2",
|
|
54
60
|
"json0-ot-diff": "^1.1.2",
|
|
61
|
+
"ngrok": "^4.3.3",
|
|
62
|
+
"open": "^9.0.0",
|
|
55
63
|
"react": "^18.2.0",
|
|
56
64
|
"react-bootstrap": "^2.7.2",
|
|
57
65
|
"react-dom": "^18.2.0",
|
|
@@ -65,6 +73,7 @@
|
|
|
65
73
|
"@types/react-dom": "^18.0.11",
|
|
66
74
|
"@vitejs/plugin-react": "^3.1.0",
|
|
67
75
|
"prettier": "^2.8.6",
|
|
68
|
-
"vite": "^4.2.1"
|
|
76
|
+
"vite": "^4.2.1",
|
|
77
|
+
"vitest": "^0.30.1"
|
|
69
78
|
}
|
|
70
79
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { useState, useEffect, useMemo, useCallback } from 'react';
|
|
2
2
|
import ShareDBClient from 'sharedb-client-browser/dist/sharedb-client-umd.cjs';
|
|
3
|
-
import { json1Presence } from '
|
|
3
|
+
import { json1Presence } from '../ot';
|
|
4
|
+
import { randomId } from '../randomId';
|
|
4
5
|
import { CodeEditor } from './CodeEditor';
|
|
5
6
|
import { diff } from './diff';
|
|
6
|
-
import { randomId } from './randomId';
|
|
7
7
|
import { Settings } from './settings';
|
|
8
|
+
import { Sidebar } from './Sidebar';
|
|
8
9
|
import './style.css';
|
|
9
10
|
|
|
10
11
|
// Register our custom JSON1 OT type that supports presence.
|
|
@@ -14,13 +15,10 @@ ShareDBClient.types.register(json1Presence.type);
|
|
|
14
15
|
// Establish the singleton ShareDB connection over WebSockets.
|
|
15
16
|
// TODO consider using reconnecting WebSocket
|
|
16
17
|
const { Connection } = ShareDBClient;
|
|
17
|
-
const
|
|
18
|
+
const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
|
|
19
|
+
const socket = new WebSocket(wsProtocol + window.location.host + '/ws');
|
|
18
20
|
const connection = new Connection(socket);
|
|
19
21
|
|
|
20
|
-
// A feature flag to disable settings for release, until it's completed.
|
|
21
|
-
// See https://vitejs.dev/guide/env-and-mode.html
|
|
22
|
-
const disableSettings = import.meta.env.VITE_DISABLE_SETTINGS === 'true';
|
|
23
|
-
|
|
24
22
|
function App() {
|
|
25
23
|
// The ShareDB document.
|
|
26
24
|
const [shareDBDoc, setShareDBDoc] = useState(null);
|
|
@@ -50,10 +48,6 @@ function App() {
|
|
|
50
48
|
// The current theme.
|
|
51
49
|
const [theme, setTheme] = useState('OneDark');
|
|
52
50
|
|
|
53
|
-
// The currently hovered over file, for showing
|
|
54
|
-
// "utils", meaning buttons for rename and delete.
|
|
55
|
-
const [utils, setUtils] = useState(false);
|
|
56
|
-
|
|
57
51
|
// True to show the settings modal.
|
|
58
52
|
const [settings, setSettings] = useState(false);
|
|
59
53
|
|
|
@@ -143,7 +137,7 @@ function App() {
|
|
|
143
137
|
);
|
|
144
138
|
|
|
145
139
|
// Called when a file in the sidebar is double-clicked.
|
|
146
|
-
const
|
|
140
|
+
const handleRenameFileClick = useCallback(
|
|
147
141
|
(key) => {
|
|
148
142
|
const newName = prompt('Enter new name');
|
|
149
143
|
if (newName) {
|
|
@@ -175,6 +169,16 @@ function App() {
|
|
|
175
169
|
[shareDBDoc, closeTab]
|
|
176
170
|
);
|
|
177
171
|
|
|
172
|
+
const handleFileClick = useCallback(
|
|
173
|
+
(fileId) => {
|
|
174
|
+
setActiveFileId(fileId);
|
|
175
|
+
if (!tabList.includes(fileId)) {
|
|
176
|
+
setTabList([...tabList, fileId]);
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
[tabList]
|
|
180
|
+
);
|
|
181
|
+
|
|
178
182
|
// TODO prompt the user "Are you sure?"
|
|
179
183
|
const handleDeleteFileClick = useCallback(
|
|
180
184
|
(fileId) => (event) => {
|
|
@@ -204,6 +208,11 @@ function App() {
|
|
|
204
208
|
setSettings(false);
|
|
205
209
|
}, []);
|
|
206
210
|
|
|
211
|
+
const fileNameSplit = (fileName) => {
|
|
212
|
+
const split = fileName.split('/');
|
|
213
|
+
return split[split.length - 1];
|
|
214
|
+
};
|
|
215
|
+
|
|
207
216
|
return (
|
|
208
217
|
<>
|
|
209
218
|
<Settings
|
|
@@ -222,7 +231,7 @@ function App() {
|
|
|
222
231
|
setActiveFileId(fileId);
|
|
223
232
|
}}
|
|
224
233
|
>
|
|
225
|
-
{tabValid ? data[fileId].name : ''}
|
|
234
|
+
{tabValid ? fileNameSplit(data[fileId].name) : ''}
|
|
226
235
|
<div
|
|
227
236
|
className={activeFileId ? 'bx bx-x tab-close' : ''}
|
|
228
237
|
onClick={handleCloseTabClick(fileId)}
|
|
@@ -230,74 +239,15 @@ function App() {
|
|
|
230
239
|
</div>
|
|
231
240
|
))}
|
|
232
241
|
</div>
|
|
233
|
-
<
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
</a>
|
|
243
|
-
</div>
|
|
244
|
-
<div>
|
|
245
|
-
<i
|
|
246
|
-
className="bx bxs-file-plus newBTN"
|
|
247
|
-
style={{ color: '#dbdde1' }}
|
|
248
|
-
onClick={createFile}
|
|
249
|
-
></i>
|
|
250
|
-
</div>
|
|
251
|
-
</div>
|
|
252
|
-
</div>
|
|
253
|
-
{data
|
|
254
|
-
? Object.keys(data).map((key) => (
|
|
255
|
-
<div className="file" key={key}>
|
|
256
|
-
<div
|
|
257
|
-
className="full-Box"
|
|
258
|
-
onClick={() => {
|
|
259
|
-
setActiveFileId(key);
|
|
260
|
-
if (!tabList.includes(key)) {
|
|
261
|
-
setTabList([...tabList, key]);
|
|
262
|
-
}
|
|
263
|
-
}}
|
|
264
|
-
onMouseEnter={() => {
|
|
265
|
-
setUtils(key);
|
|
266
|
-
}}
|
|
267
|
-
onMouseLeave={() => {
|
|
268
|
-
setUtils(null);
|
|
269
|
-
}}
|
|
270
|
-
>
|
|
271
|
-
<div className={utils === key ? 'name' : ''}>
|
|
272
|
-
{data[key].name}
|
|
273
|
-
</div>
|
|
274
|
-
<div className={utils === key ? 'utils' : 'noUtils'}>
|
|
275
|
-
<i
|
|
276
|
-
className="bx bxs-edit utilities"
|
|
277
|
-
style={{ color: '#abdafb' }}
|
|
278
|
-
onClick={() => {
|
|
279
|
-
renameFile(key);
|
|
280
|
-
}}
|
|
281
|
-
></i>
|
|
282
|
-
<i
|
|
283
|
-
className="bx bx-trash"
|
|
284
|
-
style={{ color: '#eb336c' }}
|
|
285
|
-
onClick={handleDeleteFileClick(key)}
|
|
286
|
-
></i>
|
|
287
|
-
</div>
|
|
288
|
-
</div>
|
|
289
|
-
</div>
|
|
290
|
-
))
|
|
291
|
-
: null}
|
|
292
|
-
</div>
|
|
293
|
-
</div>
|
|
294
|
-
{disableSettings ? null : (
|
|
295
|
-
<div className="settings" onClick={() => setSettings(!settings)}>
|
|
296
|
-
Settings
|
|
297
|
-
</div>
|
|
298
|
-
)}
|
|
299
|
-
</div>
|
|
300
|
-
</div>
|
|
242
|
+
<Sidebar
|
|
243
|
+
createFile={createFile}
|
|
244
|
+
data={data}
|
|
245
|
+
handleRenameFileClick={handleRenameFileClick}
|
|
246
|
+
handleDeleteFileClick={handleDeleteFileClick}
|
|
247
|
+
handleFileClick={handleFileClick}
|
|
248
|
+
setSettings={setSettings}
|
|
249
|
+
settings={settings}
|
|
250
|
+
/>
|
|
301
251
|
{data && activeFileId ? (
|
|
302
252
|
<CodeEditor
|
|
303
253
|
className="editor"
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
import { Item } from './Item';
|
|
3
|
+
import { FileOrDirectory } from './FileOrDirectory';
|
|
4
|
+
import { DirectoryArrowSVG } from './DirectoryArrowSVG';
|
|
5
|
+
|
|
6
|
+
export const Directory = ({
|
|
7
|
+
name,
|
|
8
|
+
path,
|
|
9
|
+
children,
|
|
10
|
+
handleRenameFileClick,
|
|
11
|
+
handleDeleteFileClick,
|
|
12
|
+
handleFileClick,
|
|
13
|
+
openDirectories,
|
|
14
|
+
toggleDirectory,
|
|
15
|
+
}) => {
|
|
16
|
+
const handleClick = useCallback(() => {
|
|
17
|
+
toggleDirectory(path);
|
|
18
|
+
}, [toggleDirectory]);
|
|
19
|
+
|
|
20
|
+
const handleDeleteClick = useCallback(() => {
|
|
21
|
+
console.log('TODO handleDeleteDirectoryClick');
|
|
22
|
+
}, []);
|
|
23
|
+
|
|
24
|
+
const handleRenameClick = useCallback(() => {
|
|
25
|
+
console.log('TODO handleRenameDirectoryClick');
|
|
26
|
+
}, []);
|
|
27
|
+
|
|
28
|
+
const isOpen = openDirectories[path];
|
|
29
|
+
return (
|
|
30
|
+
<>
|
|
31
|
+
<Item
|
|
32
|
+
name={name}
|
|
33
|
+
handleClick={handleClick}
|
|
34
|
+
handleDeleteClick={handleDeleteClick}
|
|
35
|
+
handleRenameClick={handleRenameClick}
|
|
36
|
+
>
|
|
37
|
+
<div
|
|
38
|
+
className="arrow-wrapper"
|
|
39
|
+
style={{ transform: `rotate(${isOpen ? 90 : 0}deg)` }}
|
|
40
|
+
>
|
|
41
|
+
<DirectoryArrowSVG />
|
|
42
|
+
</div>
|
|
43
|
+
{name}
|
|
44
|
+
</Item>
|
|
45
|
+
{children && isOpen ? (
|
|
46
|
+
<div className="indentation">
|
|
47
|
+
{children.map((entity) => (
|
|
48
|
+
<FileOrDirectory
|
|
49
|
+
entity={entity}
|
|
50
|
+
key={entity.fileId || entity.path}
|
|
51
|
+
handleRenameFileClick={handleRenameFileClick}
|
|
52
|
+
handleDeleteFileClick={handleDeleteFileClick}
|
|
53
|
+
handleFileClick={handleFileClick}
|
|
54
|
+
openDirectories={openDirectories}
|
|
55
|
+
toggleDirectory={toggleDirectory}
|
|
56
|
+
/>
|
|
57
|
+
))}
|
|
58
|
+
</div>
|
|
59
|
+
) : null}
|
|
60
|
+
</>
|
|
61
|
+
);
|
|
62
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
import { Item } from './Item';
|
|
3
|
+
|
|
4
|
+
export const File = ({
|
|
5
|
+
name,
|
|
6
|
+
fileId,
|
|
7
|
+
handleRenameFileClick,
|
|
8
|
+
handleDeleteFileClick,
|
|
9
|
+
handleFileClick,
|
|
10
|
+
}) => {
|
|
11
|
+
const handleClick = useCallback(() => {
|
|
12
|
+
handleFileClick(fileId);
|
|
13
|
+
}, [fileId, handleFileClick]);
|
|
14
|
+
|
|
15
|
+
const handleDeleteClick = useCallback(() => {
|
|
16
|
+
handleDeleteFileClick(fileId);
|
|
17
|
+
}, [fileId]);
|
|
18
|
+
|
|
19
|
+
const handleRenameClick = useCallback(() => {
|
|
20
|
+
handleRenameFileClick(fileId);
|
|
21
|
+
}, [fileId]);
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<Item
|
|
25
|
+
handleClick={handleClick}
|
|
26
|
+
handleDeleteClick={handleDeleteClick}
|
|
27
|
+
handleRenameClick={handleRenameClick}
|
|
28
|
+
>
|
|
29
|
+
{name}
|
|
30
|
+
</Item>
|
|
31
|
+
);
|
|
32
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { File } from './File';
|
|
2
|
+
import { Directory } from './Directory';
|
|
3
|
+
|
|
4
|
+
export const FileOrDirectory = ({
|
|
5
|
+
entity,
|
|
6
|
+
handleRenameFileClick,
|
|
7
|
+
handleDeleteFileClick,
|
|
8
|
+
handleFileClick,
|
|
9
|
+
openDirectories,
|
|
10
|
+
toggleDirectory,
|
|
11
|
+
}) => {
|
|
12
|
+
const { name, path, fileId, file, children } = entity;
|
|
13
|
+
return file ? (
|
|
14
|
+
<File
|
|
15
|
+
fileId={fileId}
|
|
16
|
+
name={name}
|
|
17
|
+
handleRenameFileClick={handleRenameFileClick}
|
|
18
|
+
handleDeleteFileClick={handleDeleteFileClick}
|
|
19
|
+
handleFileClick={handleFileClick}
|
|
20
|
+
/>
|
|
21
|
+
) : (
|
|
22
|
+
<Directory
|
|
23
|
+
name={name}
|
|
24
|
+
path={path}
|
|
25
|
+
children={children}
|
|
26
|
+
handleRenameFileClick={handleRenameFileClick}
|
|
27
|
+
handleDeleteFileClick={handleDeleteFileClick}
|
|
28
|
+
handleFileClick={handleFileClick}
|
|
29
|
+
openDirectories={openDirectories}
|
|
30
|
+
toggleDirectory={toggleDirectory}
|
|
31
|
+
/>
|
|
32
|
+
);
|
|
33
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { useState, useCallback } from 'react';
|
|
2
|
+
|
|
3
|
+
export const Item = ({
|
|
4
|
+
children,
|
|
5
|
+
handleClick,
|
|
6
|
+
handleRenameClick,
|
|
7
|
+
handleDeleteClick,
|
|
8
|
+
}) => {
|
|
9
|
+
const [isHovered, setIsHovered] = useState(false);
|
|
10
|
+
|
|
11
|
+
const handleMouseEnter = useCallback(() => {
|
|
12
|
+
setIsHovered(true);
|
|
13
|
+
}, []);
|
|
14
|
+
|
|
15
|
+
const handleMouseLeave = useCallback(() => {
|
|
16
|
+
setIsHovered(false);
|
|
17
|
+
}, []);
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div
|
|
21
|
+
className="full-box file-or-directory"
|
|
22
|
+
onClick={handleClick}
|
|
23
|
+
onMouseEnter={handleMouseEnter}
|
|
24
|
+
onMouseLeave={handleMouseLeave}
|
|
25
|
+
>
|
|
26
|
+
<div className="name">{children}</div>
|
|
27
|
+
{isHovered ? (
|
|
28
|
+
<div className="utils">
|
|
29
|
+
<i
|
|
30
|
+
className="bx bxs-edit utilities"
|
|
31
|
+
style={{ color: '#abdafb' }}
|
|
32
|
+
onClick={handleRenameClick}
|
|
33
|
+
></i>
|
|
34
|
+
<i
|
|
35
|
+
className="bx bx-trash"
|
|
36
|
+
style={{ color: '#eb336c' }}
|
|
37
|
+
onClick={handleDeleteClick}
|
|
38
|
+
></i>
|
|
39
|
+
</div>
|
|
40
|
+
) : null}
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { getFileTree } from '../getFileTree';
|
|
3
|
+
import { sortFileTree } from '../sortFileTree';
|
|
4
|
+
import { disableSettings } from '../featureFlags';
|
|
5
|
+
import { FileOrDirectory } from './FileOrDirectory';
|
|
6
|
+
import { useOpenDirectories } from './useOpenDirectories';
|
|
7
|
+
|
|
8
|
+
import './styles.css';
|
|
9
|
+
|
|
10
|
+
export const Sidebar = ({
|
|
11
|
+
createFile,
|
|
12
|
+
data,
|
|
13
|
+
handleRenameFileClick,
|
|
14
|
+
handleDeleteFileClick,
|
|
15
|
+
handleFileClick,
|
|
16
|
+
setSettings,
|
|
17
|
+
settings,
|
|
18
|
+
}) => {
|
|
19
|
+
const fileTree = useMemo(
|
|
20
|
+
() => (data ? sortFileTree(getFileTree(data)) : null),
|
|
21
|
+
[data]
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const { openDirectories, toggleDirectory } = useOpenDirectories();
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className="vz-sidebar">
|
|
28
|
+
<div className="files">
|
|
29
|
+
<div className="full-box">
|
|
30
|
+
<div>
|
|
31
|
+
<a className="link-name" href="#">
|
|
32
|
+
Files
|
|
33
|
+
</a>
|
|
34
|
+
</div>
|
|
35
|
+
<div>
|
|
36
|
+
<i
|
|
37
|
+
className="bx bxs-file-plus new-btn"
|
|
38
|
+
style={{ color: '#dbdde1' }}
|
|
39
|
+
onClick={createFile}
|
|
40
|
+
></i>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
{fileTree
|
|
45
|
+
? fileTree.children.map((entity) => (
|
|
46
|
+
<FileOrDirectory
|
|
47
|
+
entity={entity}
|
|
48
|
+
key={entity.fileId || entity.path}
|
|
49
|
+
handleRenameFileClick={handleRenameFileClick}
|
|
50
|
+
handleDeleteFileClick={handleDeleteFileClick}
|
|
51
|
+
handleFileClick={handleFileClick}
|
|
52
|
+
openDirectories={openDirectories}
|
|
53
|
+
toggleDirectory={toggleDirectory}
|
|
54
|
+
/>
|
|
55
|
+
))
|
|
56
|
+
: null}
|
|
57
|
+
{disableSettings ? null : (
|
|
58
|
+
<div className="settings" onClick={() => setSettings(!settings)}>
|
|
59
|
+
Settings
|
|
60
|
+
</div>
|
|
61
|
+
)}
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
.vz-sidebar {
|
|
2
|
+
/* TODO remove position: fixed, overhaul CSS to use Flexbox for entire app */
|
|
3
|
+
|
|
4
|
+
position: fixed;
|
|
5
|
+
top: 0;
|
|
6
|
+
left: 0;
|
|
7
|
+
height: 100%;
|
|
8
|
+
width: 200px;
|
|
9
|
+
background: rgb(54, 65, 89);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.vz-sidebar .full-box {
|
|
13
|
+
padding: 7px;
|
|
14
|
+
display: flex;
|
|
15
|
+
flex-direction: row;
|
|
16
|
+
align-items: center;
|
|
17
|
+
justify-content: space-between;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.vz-sidebar .file-or-directory {
|
|
21
|
+
display: flex;
|
|
22
|
+
flex-direction: row;
|
|
23
|
+
align-items: center;
|
|
24
|
+
justify-content: space-between;
|
|
25
|
+
cursor: pointer;
|
|
26
|
+
margin: 0 10px;
|
|
27
|
+
color: rgb(219, 221, 225);
|
|
28
|
+
border-radius: 5px;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.vz-sidebar .link-name {
|
|
32
|
+
margin-top: 30px;
|
|
33
|
+
margin-left: 10px;
|
|
34
|
+
color: rgb(154, 160, 172);
|
|
35
|
+
font-size: 12px;
|
|
36
|
+
text-decoration: none;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.vz-sidebar .new-btn {
|
|
40
|
+
justify-content: right;
|
|
41
|
+
margin-right: 10px;
|
|
42
|
+
cursor: pointer;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.vz-sidebar .utils {
|
|
46
|
+
display: flex;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.vz-sidebar .utilities {
|
|
50
|
+
margin-right: 3px;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.vz-sidebar .settings {
|
|
54
|
+
position: absolute;
|
|
55
|
+
bottom: 0;
|
|
56
|
+
height: 30px;
|
|
57
|
+
width: 180px;
|
|
58
|
+
z-index: 100;
|
|
59
|
+
color: rgb(219, 221, 225);
|
|
60
|
+
cursor: pointer;
|
|
61
|
+
display: flex;
|
|
62
|
+
align-items: center;
|
|
63
|
+
justify-content: center;
|
|
64
|
+
padding: 14px 0;
|
|
65
|
+
margin: 10px;
|
|
66
|
+
border-radius: 5px;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.vz-sidebar .file-or-directory:hover,
|
|
70
|
+
.vz-sidebar .settings:hover {
|
|
71
|
+
background-color: rgb(74, 84, 105);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.vz-sidebar .name {
|
|
75
|
+
display: flex;
|
|
76
|
+
text-overflow: ellipsis;
|
|
77
|
+
white-space: nowrap;
|
|
78
|
+
overflow: hidden;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* Responsible for indenting directories */
|
|
82
|
+
.vz-sidebar .indentation {
|
|
83
|
+
margin-left: 20px;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.vz-sidebar .arrow-wrapper {
|
|
87
|
+
margin-right: 4px;
|
|
88
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { useState, useCallback } from 'react';
|
|
2
|
+
|
|
3
|
+
// TODO bring this feature back
|
|
4
|
+
// When a page is opened with an active file,
|
|
5
|
+
// make sure all the directories leading to that file
|
|
6
|
+
// are opened automatically.
|
|
7
|
+
//export const initialOpenDirectories = (activeFile) => {
|
|
8
|
+
// const openDirectories = {};
|
|
9
|
+
// if (activeFile) {
|
|
10
|
+
// const path = activeFile.split('/');
|
|
11
|
+
// for (let i = 1; i < path.length; i++) {
|
|
12
|
+
// openDirectories[path.slice(0, i).join('/')] = true;
|
|
13
|
+
// }
|
|
14
|
+
// }
|
|
15
|
+
// return openDirectories;
|
|
16
|
+
//};
|
|
17
|
+
|
|
18
|
+
// Inspired by
|
|
19
|
+
// https://github.com/vizhub-core/vizhub/blob/main/vizhub-v2/packages/neoFrontend/src/pages/VizPage/Body/Editor/FilesSection/useOpenDirectories.js
|
|
20
|
+
export const useOpenDirectories = (activeFile) => {
|
|
21
|
+
//const [openDirectories, setOpenDirectories] = useState(
|
|
22
|
+
// initialOpenDirectories(activeFile)
|
|
23
|
+
//);
|
|
24
|
+
const [openDirectories, setOpenDirectories] = useState({});
|
|
25
|
+
|
|
26
|
+
// TODO consider useReducer to avoid event listener churn
|
|
27
|
+
const toggleDirectory = useCallback(
|
|
28
|
+
(path) => {
|
|
29
|
+
console.log(openDirectories[path], 'here');
|
|
30
|
+
setOpenDirectories(
|
|
31
|
+
Object.assign({}, openDirectories, {
|
|
32
|
+
[path]: !openDirectories[path],
|
|
33
|
+
})
|
|
34
|
+
);
|
|
35
|
+
},
|
|
36
|
+
[openDirectories]
|
|
37
|
+
);
|
|
38
|
+
return { openDirectories, toggleDirectory };
|
|
39
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import jsondiff from 'json0-ot-diff';
|
|
2
2
|
import diffMatchPatch from 'diff-match-patch';
|
|
3
|
-
import { json1Presence, textUnicode } from '
|
|
3
|
+
import { json1Presence, textUnicode } from '../ot';
|
|
4
4
|
|
|
5
5
|
export const diff = (a, b) =>
|
|
6
6
|
jsondiff(a, b, diffMatchPatch, json1Presence, textUnicode);
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Inspired by
|
|
2
|
+
// https://github.com/vizhub-core/vizhub/blob/main/vizhub-v2/packages/neoFrontend/src/pages/VizPage/Body/Editor/FilesSection/FileTree/getFileTree.js
|
|
3
|
+
export const getFileTree = (files) => {
|
|
4
|
+
const tree = { name: 'files' };
|
|
5
|
+
for (const fileId of Object.keys(files)) {
|
|
6
|
+
const file = files[fileId];
|
|
7
|
+
const path = file.name.split('/');
|
|
8
|
+
const n = path.length;
|
|
9
|
+
let node = tree;
|
|
10
|
+
|
|
11
|
+
// Walk the path.
|
|
12
|
+
for (let i = 0; i < n - 1; i++) {
|
|
13
|
+
// Search for an existing child.
|
|
14
|
+
let child;
|
|
15
|
+
const name = path[i];
|
|
16
|
+
if (node.children) {
|
|
17
|
+
for (let j = 0; j < node.children.length; j++) {
|
|
18
|
+
const nodeChild = node.children[j];
|
|
19
|
+
if (nodeChild.name === name) {
|
|
20
|
+
child = nodeChild;
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Create a child if none with matching name exists.
|
|
27
|
+
if (!child) {
|
|
28
|
+
child = {
|
|
29
|
+
name: path[i],
|
|
30
|
+
path: path.slice(0, i + 1).join('/'),
|
|
31
|
+
};
|
|
32
|
+
(node.children || (node.children = [])).push(child);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
node = child;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// file.text being `null` signifies
|
|
39
|
+
// that this is a directory.
|
|
40
|
+
if (file.text !== null) {
|
|
41
|
+
(node.children || (node.children = [])).push({
|
|
42
|
+
name: path[n - 1],
|
|
43
|
+
file,
|
|
44
|
+
fileId,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return tree;
|
|
49
|
+
};
|