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/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-4694dca8.js"></script>
8
- <link rel="stylesheet" href="/assets/index-58524f84.css">
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.0",
3
+ "version": "0.5.0",
4
4
  "description": "Multiplayer code editor system",
5
5
  "main": "index.js",
6
6
  "type": "module",
7
- "bin": "cli.js",
7
+ "bin": "src/server/index.js",
8
8
  "files": [
9
9
  "dist/**",
10
10
  "src/**"
11
11
  ],
12
12
  "scripts": {
13
- "test": "npm run build; cd test; node ../cli.js",
14
- "prettier": "prettier {*.*,**/*.*} --single-quote --write",
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
- "author": "Curran Kelleher",
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 './ot';
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 socket = new WebSocket('ws://' + window.location.host + '/ws');
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 renameFile = useCallback(
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
- <div className="sidebar show">
234
- <div className="nav-links">
235
- <div className="show-menu">
236
- <div className="sub-menu">
237
- <div className="files">
238
- <div className="full-Box">
239
- <div>
240
- <a className="link-name" href="#">
241
- Files
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,5 @@
1
+ export const DirectoryArrowSVG = ({ height = 10, fill = 'currentcolor' }) => (
2
+ <svg height={height} viewBox={'4 4 16 16'}>
3
+ <path fill={fill} d="M8 17l9-5-9-5v10z" />
4
+ </svg>
5
+ );
@@ -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 './ot';
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,3 @@
1
+ // A feature flag to disable settings for release, until it's completed.
2
+ // See https://vitejs.dev/guide/env-and-mode.html
3
+ export const disableSettings = import.meta.env.VITE_DISABLE_SETTINGS === 'true';
@@ -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
+ };