vzcode 0.5.0 → 0.6.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 +21 -5
- package/dist/assets/index-41607816.js +89 -0
- package/dist/assets/index-8bac68c9.css +1 -0
- package/dist/index.html +16 -2
- package/package.json +25 -20
- package/src/client/{App.jsx → App.tsx} +68 -60
- package/src/client/Sidebar/{Directory.jsx → DirectoryListing.tsx} +27 -13
- package/src/client/Sidebar/{File.jsx → FileListing.tsx} +14 -4
- package/src/client/Sidebar/Listing.tsx +44 -0
- package/src/client/Sidebar/index.tsx +80 -0
- package/src/client/Sidebar/styles.scss +84 -0
- package/src/client/Sidebar/{useOpenDirectories.js → useOpenDirectories.ts} +4 -6
- package/src/client/{getFileTree.js → getFileTree.ts} +6 -3
- package/src/client/getOrCreateEditor.js +5 -4
- package/src/client/settings.jsx +22 -66
- package/src/client/{sortFileTree.js → sortFileTree.ts} +7 -3
- package/src/client/style.scss +93 -0
- package/src/server/index.js +0 -0
- package/src/types.ts +57 -0
- package/dist/assets/index-29b6a606.css +0 -1
- package/dist/assets/index-523ecad7.js +0 -89
- package/src/client/Sidebar/FileOrDirectory.jsx +0 -33
- package/src/client/Sidebar/index.jsx +0 -64
- package/src/client/Sidebar/styles.css +0 -88
- package/src/client/style.css +0 -120
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.vz-sidebar{height:100%;width:200px;background:rgb(54,65,89);display:flex;flex-direction:column;justify-content:space-between}.vz-sidebar .full-box{padding:7px;display:flex;flex-direction:row;align-items:center;justify-content:space-between}.vz-sidebar .files{overflow:auto}.vz-sidebar .file-or-directory{display:flex;flex-direction:row;align-items:center;justify-content:space-between;cursor:pointer;margin:0 10px;color:#dbdde1;border-radius:5px}.vz-sidebar .link-name{margin-top:30px;margin-left:10px;color:#9aa0ac;font-size:12px;text-decoration:none}.vz-sidebar .new-btn{justify-content:right;margin-right:10px;cursor:pointer}.vz-sidebar .utils{display:flex}.vz-sidebar .utilities{margin-right:3px}.vz-sidebar .file-or-directory:hover,.vz-sidebar .settings:hover{background-color:#4a5469}.vz-sidebar .name{display:flex;text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.vz-sidebar .indentation{margin-left:20px}.vz-sidebar .arrow-wrapper{margin-right:4px}.vz-sidebar .settings{height:30px;color:#dbdde1;cursor:pointer;display:flex;align-items:center;justify-content:center;padding:14px 0;margin:10px;border-radius:5px}:root{--vzcode-font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace}::-webkit-scrollbar{width:10px;height:10px}::-webkit-scrollbar-track{background:rgb(41,44,52)}::-webkit-scrollbar-thumb{background:rgb(37,37,38)}::-webkit-scrollbar-thumb:hover{background:#555}html,body,#root,.app{height:100%}.app{background-color:#292c34;font-family:Poppins,sans-serif;display:flex}.app .left{display:flex}.app .right{display:flex;flex:1;flex-direction:column;min-width:0}.app .right .tab-list{display:flex;background-color:#252526}.app .right .tab-list .tab{background:rgb(45,45,45);color:#fff;font-family:var(--vzcode-font-family);align-items:center;display:flex;padding-top:2px;padding-left:10px;padding-right:10px;border-top-left-radius:10px;border-top-right-radius:10px;margin-top:5px;margin-left:2px;margin-right:2px;cursor:pointer}.app .right .tab-list .tab .tab-close{margin-left:5px}.app .right .tab-list .tab .tab-close:hover{border-radius:30%;background-color:#252526}.app .right .tab-list .tab.active{background-color:#292c34}.app .right .editor{flex:1;display:flex;font-family:var(--vzcode-font-family);overflow-x:auto}.themes{color:#252526}
|
package/dist/index.html
CHANGED
|
@@ -4,8 +4,22 @@
|
|
|
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
|
-
<
|
|
8
|
-
<link rel="
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
8
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
9
|
+
<link
|
|
10
|
+
href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap"
|
|
11
|
+
rel="stylesheet"
|
|
12
|
+
/>
|
|
13
|
+
<link
|
|
14
|
+
href="https://unpkg.com/boxicons@2.0.7/css/boxicons.min.css"
|
|
15
|
+
rel="stylesheet"
|
|
16
|
+
/>
|
|
17
|
+
<link
|
|
18
|
+
href="https://unpkg.com/bootstrap@5.3.0/dist/css/bootstrap.min.css"
|
|
19
|
+
rel="stylesheet"
|
|
20
|
+
/>
|
|
21
|
+
<script type="module" crossorigin src="/assets/index-41607816.js"></script>
|
|
22
|
+
<link rel="stylesheet" href="/assets/index-8bac68c9.css">
|
|
9
23
|
</head>
|
|
10
24
|
<body>
|
|
11
25
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vzcode",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Multiplayer code editor system",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
7
|
+
"types": "src/types.ts",
|
|
7
8
|
"bin": "src/server/index.js",
|
|
8
9
|
"files": [
|
|
9
10
|
"dist/**",
|
|
@@ -13,6 +14,7 @@
|
|
|
13
14
|
"test": "vitest",
|
|
14
15
|
"test-interactive": "npm run build; cd test/sampleDirectories/kitchenSink; node ../../../src/server/index.js",
|
|
15
16
|
"prettier": "prettier {*.*,**/*.*} --write",
|
|
17
|
+
"tsc": "tsc --noEmit",
|
|
16
18
|
"dev": "vite",
|
|
17
19
|
"build": "vite build",
|
|
18
20
|
"build-release": "vite build --mode release",
|
|
@@ -44,36 +46,39 @@
|
|
|
44
46
|
},
|
|
45
47
|
"homepage": "https://github.com/vizhub-core/vzcode#readme",
|
|
46
48
|
"dependencies": {
|
|
47
|
-
"@codemirror/lang-css": "^6.
|
|
48
|
-
"@codemirror/lang-html": "^6.4.
|
|
49
|
-
"@codemirror/lang-javascript": "^6.1.
|
|
50
|
-
"@codemirror/lang-markdown": "^6.1.
|
|
51
|
-
"@codemirror/state": "^6.2.
|
|
52
|
-
"@codemirror/theme-one-dark": "^6.1.
|
|
49
|
+
"@codemirror/lang-css": "^6.2.0",
|
|
50
|
+
"@codemirror/lang-html": "^6.4.4",
|
|
51
|
+
"@codemirror/lang-javascript": "^6.1.9",
|
|
52
|
+
"@codemirror/lang-markdown": "^6.1.1",
|
|
53
|
+
"@codemirror/state": "^6.2.1",
|
|
54
|
+
"@codemirror/theme-one-dark": "^6.1.2",
|
|
53
55
|
"@teamwork/websocket-json-stream": "^2.0.0",
|
|
56
|
+
"@uiw/codemirror-themes-all": "^4.20.2",
|
|
54
57
|
"codemirror": "^6.0.1",
|
|
55
58
|
"codemirror-ot": "^4.0.0",
|
|
56
|
-
"d3-array": "^3.2.
|
|
59
|
+
"d3-array": "^3.2.4",
|
|
57
60
|
"diff-match-patch": "^1.0.5",
|
|
58
|
-
"dotenv": "^16.0
|
|
61
|
+
"dotenv": "^16.3.0",
|
|
59
62
|
"express": "^4.18.2",
|
|
60
63
|
"json0-ot-diff": "^1.1.2",
|
|
61
64
|
"ngrok": "^4.3.3",
|
|
62
|
-
"open": "^9.
|
|
65
|
+
"open": "^9.1.0",
|
|
63
66
|
"react": "^18.2.0",
|
|
64
|
-
"react-bootstrap": "^2.7.
|
|
67
|
+
"react-bootstrap": "^2.7.4",
|
|
65
68
|
"react-dom": "^18.2.0",
|
|
66
|
-
"react-select": "^5.7.
|
|
67
|
-
"sharedb": "^
|
|
68
|
-
"sharedb-client-browser": "^4.2.
|
|
69
|
+
"react-select": "^5.7.3",
|
|
70
|
+
"sharedb": "^4.0.0",
|
|
71
|
+
"sharedb-client-browser": "^4.2.1",
|
|
69
72
|
"ws": "^8.13.0"
|
|
70
73
|
},
|
|
71
74
|
"devDependencies": {
|
|
72
|
-
"@types/react": "^18.
|
|
73
|
-
"@types/react-dom": "^18.
|
|
74
|
-
"@vitejs/plugin-react": "^
|
|
75
|
-
"prettier": "^2.8.
|
|
76
|
-
"
|
|
77
|
-
"
|
|
75
|
+
"@types/react": "^18.2.12",
|
|
76
|
+
"@types/react-dom": "^18.2.5",
|
|
77
|
+
"@vitejs/plugin-react": "^4.0.0",
|
|
78
|
+
"prettier": "^2.8.8",
|
|
79
|
+
"sass": "^1.64.0",
|
|
80
|
+
"vite": "^4.3.9",
|
|
81
|
+
"vitest": "^0.32.2",
|
|
82
|
+
"typescript": "^5.1.6"
|
|
78
83
|
}
|
|
79
84
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useEffect,
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
2
2
|
import ShareDBClient from 'sharedb-client-browser/dist/sharedb-client-umd.cjs';
|
|
3
3
|
import { json1Presence } from '../ot';
|
|
4
4
|
import { randomId } from '../randomId';
|
|
@@ -6,7 +6,9 @@ import { CodeEditor } from './CodeEditor';
|
|
|
6
6
|
import { diff } from './diff';
|
|
7
7
|
import { Settings } from './settings';
|
|
8
8
|
import { Sidebar } from './Sidebar';
|
|
9
|
-
import './style.
|
|
9
|
+
import './style.scss';
|
|
10
|
+
import { oneDark } from '@codemirror/theme-one-dark';
|
|
11
|
+
import { FileId } from '../types';
|
|
10
12
|
|
|
11
13
|
// Register our custom JSON1 OT type that supports presence.
|
|
12
14
|
// See https://github.com/vizhub-core/json1-presence
|
|
@@ -36,9 +38,6 @@ function App() {
|
|
|
36
38
|
// updated on each change to decouple rendering from ShareDB.
|
|
37
39
|
const [data, setData] = useState(null);
|
|
38
40
|
|
|
39
|
-
// True if the file menu is open.
|
|
40
|
-
const [isFileMenuOpen, setIsFileMenuOpen] = useState(false);
|
|
41
|
-
|
|
42
41
|
// The id of the currently open file tab.
|
|
43
42
|
const [activeFileId, setActiveFileId] = useState(null);
|
|
44
43
|
|
|
@@ -46,10 +45,10 @@ function App() {
|
|
|
46
45
|
const [tabList, setTabList] = useState([]);
|
|
47
46
|
|
|
48
47
|
// The current theme.
|
|
49
|
-
const [theme, setTheme] = useState(
|
|
48
|
+
const [theme, setTheme] = useState(oneDark);
|
|
50
49
|
|
|
51
50
|
// True to show the settings modal.
|
|
52
|
-
const [
|
|
51
|
+
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
|
53
52
|
|
|
54
53
|
// Set up the connection to ShareDB.
|
|
55
54
|
useEffect(() => {
|
|
@@ -138,14 +137,14 @@ function App() {
|
|
|
138
137
|
|
|
139
138
|
// Called when a file in the sidebar is double-clicked.
|
|
140
139
|
const handleRenameFileClick = useCallback(
|
|
141
|
-
(
|
|
140
|
+
(fileId: FileId) => {
|
|
142
141
|
const newName = prompt('Enter new name');
|
|
143
142
|
if (newName) {
|
|
144
143
|
const currentDocument = shareDBDoc.data;
|
|
145
144
|
const nextDocument = {
|
|
146
145
|
...currentDocument,
|
|
147
|
-
[
|
|
148
|
-
...currentDocument[
|
|
146
|
+
[fileId]: {
|
|
147
|
+
...currentDocument[fileId],
|
|
149
148
|
name: newName,
|
|
150
149
|
},
|
|
151
150
|
};
|
|
@@ -156,21 +155,21 @@ function App() {
|
|
|
156
155
|
);
|
|
157
156
|
|
|
158
157
|
const deleteFile = useCallback(
|
|
159
|
-
(
|
|
158
|
+
(fileId: FileId) => {
|
|
160
159
|
// Close the tab in case it's open.
|
|
161
160
|
// TODO consistently use either "key" or "fileId" naming
|
|
162
|
-
closeTab(
|
|
161
|
+
closeTab(fileId);
|
|
163
162
|
|
|
164
163
|
const currentDocument = shareDBDoc.data;
|
|
165
164
|
const nextDocument = { ...currentDocument };
|
|
166
|
-
delete nextDocument[
|
|
165
|
+
delete nextDocument[fileId];
|
|
167
166
|
shareDBDoc.submitOp(diff(currentDocument, nextDocument));
|
|
168
167
|
},
|
|
169
168
|
[shareDBDoc, closeTab]
|
|
170
169
|
);
|
|
171
170
|
|
|
172
171
|
const handleFileClick = useCallback(
|
|
173
|
-
(fileId) => {
|
|
172
|
+
(fileId: FileId) => {
|
|
174
173
|
setActiveFileId(fileId);
|
|
175
174
|
if (!tabList.includes(fileId)) {
|
|
176
175
|
setTabList([...tabList, fileId]);
|
|
@@ -181,7 +180,7 @@ function App() {
|
|
|
181
180
|
|
|
182
181
|
// TODO prompt the user "Are you sure?"
|
|
183
182
|
const handleDeleteFileClick = useCallback(
|
|
184
|
-
(fileId
|
|
183
|
+
(fileId: FileId, event: React.MouseEvent) => {
|
|
185
184
|
// Stop propagation so that the outer listener doesn't fire,
|
|
186
185
|
// which would try to open this file in a tab.
|
|
187
186
|
event.stopPropagation();
|
|
@@ -191,6 +190,7 @@ function App() {
|
|
|
191
190
|
);
|
|
192
191
|
|
|
193
192
|
const createFile = useCallback(() => {
|
|
193
|
+
// TODO better UI, maybe Bootstrap modal? Maybe edit inline?
|
|
194
194
|
const name = prompt('Enter new file name');
|
|
195
195
|
if (!name) return;
|
|
196
196
|
const currentDocument = shareDBDoc.data;
|
|
@@ -205,60 +205,68 @@ function App() {
|
|
|
205
205
|
const tabValid = data && activeFileId;
|
|
206
206
|
|
|
207
207
|
const handleSettingsClose = useCallback(() => {
|
|
208
|
-
|
|
208
|
+
setIsSettingsOpen(false);
|
|
209
209
|
}, []);
|
|
210
210
|
|
|
211
211
|
const fileNameSplit = (fileName) => {
|
|
212
212
|
const split = fileName.split('/');
|
|
213
|
-
|
|
213
|
+
//adding the folder that the is in to the tab name
|
|
214
|
+
if (split.length === 1) return split[split.length - 1];
|
|
215
|
+
return split[split.length - 2] + '/' + split[split.length - 1];
|
|
214
216
|
};
|
|
215
217
|
|
|
216
218
|
return (
|
|
217
|
-
|
|
218
|
-
<
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
219
|
+
<div className="app">
|
|
220
|
+
<div className="left">
|
|
221
|
+
<Sidebar
|
|
222
|
+
createFile={createFile}
|
|
223
|
+
files={data}
|
|
224
|
+
handleRenameFileClick={handleRenameFileClick}
|
|
225
|
+
handleDeleteFileClick={handleDeleteFileClick}
|
|
226
|
+
handleFileClick={handleFileClick}
|
|
227
|
+
setIsSettingsOpen={setIsSettingsOpen}
|
|
228
|
+
isSettingsOpen={isSettingsOpen}
|
|
229
|
+
/>
|
|
230
|
+
<Settings
|
|
231
|
+
show={isSettingsOpen}
|
|
232
|
+
onClose={handleSettingsClose}
|
|
233
|
+
setTheme={setTheme}
|
|
234
|
+
/>
|
|
235
|
+
</div>
|
|
236
|
+
<div className="right">
|
|
237
|
+
<div className="tab-list">
|
|
238
|
+
{tabList.map((fileId) => (
|
|
235
239
|
<div
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
240
|
+
key={fileId}
|
|
241
|
+
className={
|
|
242
|
+
tabValid
|
|
243
|
+
? `tab${fileId === activeFileId ? ' active' : ''}`
|
|
244
|
+
: null
|
|
245
|
+
}
|
|
246
|
+
onClick={() => {
|
|
247
|
+
setActiveFileId(fileId);
|
|
248
|
+
}}
|
|
249
|
+
>
|
|
250
|
+
{tabValid ? fileNameSplit(data[fileId].name) : ''}
|
|
251
|
+
<div
|
|
252
|
+
className={activeFileId ? 'bx bx-x tab-close' : ''}
|
|
253
|
+
onClick={handleCloseTabClick(fileId)}
|
|
254
|
+
></div>
|
|
255
|
+
</div>
|
|
256
|
+
))}
|
|
257
|
+
</div>
|
|
258
|
+
{data && activeFileId ? (
|
|
259
|
+
<CodeEditor
|
|
260
|
+
className="editor"
|
|
261
|
+
shareDBDoc={shareDBDoc}
|
|
262
|
+
localPresence={localPresence}
|
|
263
|
+
docPresence={docPresence}
|
|
264
|
+
activeFileId={activeFileId}
|
|
265
|
+
theme={theme}
|
|
266
|
+
/>
|
|
267
|
+
) : null}
|
|
241
268
|
</div>
|
|
242
|
-
|
|
243
|
-
createFile={createFile}
|
|
244
|
-
data={data}
|
|
245
|
-
handleRenameFileClick={handleRenameFileClick}
|
|
246
|
-
handleDeleteFileClick={handleDeleteFileClick}
|
|
247
|
-
handleFileClick={handleFileClick}
|
|
248
|
-
setSettings={setSettings}
|
|
249
|
-
settings={settings}
|
|
250
|
-
/>
|
|
251
|
-
{data && activeFileId ? (
|
|
252
|
-
<CodeEditor
|
|
253
|
-
className="editor"
|
|
254
|
-
shareDBDoc={shareDBDoc}
|
|
255
|
-
localPresence={localPresence}
|
|
256
|
-
docPresence={docPresence}
|
|
257
|
-
activeFileId={activeFileId}
|
|
258
|
-
theme={theme}
|
|
259
|
-
/>
|
|
260
|
-
) : null}
|
|
261
|
-
</>
|
|
269
|
+
</div>
|
|
262
270
|
);
|
|
263
271
|
}
|
|
264
272
|
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { useCallback } from 'react';
|
|
2
2
|
import { Item } from './Item';
|
|
3
|
-
import {
|
|
3
|
+
import { Listing } from './Listing';
|
|
4
4
|
import { DirectoryArrowSVG } from './DirectoryArrowSVG';
|
|
5
|
+
import { FileTree, FileTreeFile } from '../../types';
|
|
5
6
|
|
|
6
|
-
export const
|
|
7
|
+
export const DirectoryListing = ({
|
|
7
8
|
name,
|
|
8
9
|
path,
|
|
9
10
|
children,
|
|
@@ -12,6 +13,15 @@ export const Directory = ({
|
|
|
12
13
|
handleFileClick,
|
|
13
14
|
openDirectories,
|
|
14
15
|
toggleDirectory,
|
|
16
|
+
}: {
|
|
17
|
+
name: string;
|
|
18
|
+
path: string;
|
|
19
|
+
children: Array<FileTree | FileTreeFile>;
|
|
20
|
+
handleRenameFileClick: (fileId: string) => void;
|
|
21
|
+
handleDeleteFileClick: (fileId: string, event: React.MouseEvent) => void;
|
|
22
|
+
handleFileClick: (fileId: string) => void;
|
|
23
|
+
openDirectories: { [path: string]: boolean };
|
|
24
|
+
toggleDirectory: (path: string) => void;
|
|
15
25
|
}) => {
|
|
16
26
|
const handleClick = useCallback(() => {
|
|
17
27
|
toggleDirectory(path);
|
|
@@ -44,17 +54,21 @@ export const Directory = ({
|
|
|
44
54
|
</Item>
|
|
45
55
|
{children && isOpen ? (
|
|
46
56
|
<div className="indentation">
|
|
47
|
-
{children.map((entity) =>
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
{children.map((entity) => {
|
|
58
|
+
const { fileId } = entity as FileTreeFile;
|
|
59
|
+
const { path } = entity as FileTree;
|
|
60
|
+
return (
|
|
61
|
+
<Listing
|
|
62
|
+
entity={entity}
|
|
63
|
+
key={fileId || path}
|
|
64
|
+
handleRenameFileClick={handleRenameFileClick}
|
|
65
|
+
handleDeleteFileClick={handleDeleteFileClick}
|
|
66
|
+
handleFileClick={handleFileClick}
|
|
67
|
+
openDirectories={openDirectories}
|
|
68
|
+
toggleDirectory={toggleDirectory}
|
|
69
|
+
/>
|
|
70
|
+
);
|
|
71
|
+
})}
|
|
58
72
|
</div>
|
|
59
73
|
) : null}
|
|
60
74
|
</>
|
|
@@ -1,20 +1,30 @@
|
|
|
1
1
|
import { useCallback } from 'react';
|
|
2
2
|
import { Item } from './Item';
|
|
3
|
+
import { FileId } from '../../types';
|
|
3
4
|
|
|
4
|
-
export const
|
|
5
|
+
export const FileListing = ({
|
|
5
6
|
name,
|
|
6
7
|
fileId,
|
|
7
8
|
handleRenameFileClick,
|
|
8
9
|
handleDeleteFileClick,
|
|
9
10
|
handleFileClick,
|
|
11
|
+
}: {
|
|
12
|
+
name: string;
|
|
13
|
+
fileId: FileId;
|
|
14
|
+
handleRenameFileClick: (fileId: FileId) => void;
|
|
15
|
+
handleDeleteFileClick: (fileId: FileId, event: React.MouseEvent) => void;
|
|
16
|
+
handleFileClick: (fileId: FileId) => void;
|
|
10
17
|
}) => {
|
|
11
18
|
const handleClick = useCallback(() => {
|
|
12
19
|
handleFileClick(fileId);
|
|
13
20
|
}, [fileId, handleFileClick]);
|
|
14
21
|
|
|
15
|
-
const handleDeleteClick = useCallback(
|
|
16
|
-
|
|
17
|
-
|
|
22
|
+
const handleDeleteClick = useCallback(
|
|
23
|
+
(event) => {
|
|
24
|
+
handleDeleteFileClick(fileId, event);
|
|
25
|
+
},
|
|
26
|
+
[fileId]
|
|
27
|
+
);
|
|
18
28
|
|
|
19
29
|
const handleRenameClick = useCallback(() => {
|
|
20
30
|
handleRenameFileClick(fileId);
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { DirectoryListing } from './DirectoryListing';
|
|
2
|
+
import { FileId, FileTree, FileTreeFile } from '../../types';
|
|
3
|
+
import { FileListing } from './FileListing';
|
|
4
|
+
|
|
5
|
+
// A "Listing" is a "FileListing" or a "DirectoryListing"
|
|
6
|
+
// that appears in the Sidebar.
|
|
7
|
+
export const Listing = ({
|
|
8
|
+
entity,
|
|
9
|
+
handleRenameFileClick,
|
|
10
|
+
handleDeleteFileClick,
|
|
11
|
+
handleFileClick,
|
|
12
|
+
openDirectories,
|
|
13
|
+
toggleDirectory,
|
|
14
|
+
}: {
|
|
15
|
+
entity: FileTree | FileTreeFile;
|
|
16
|
+
handleRenameFileClick: (fileId: FileId) => void;
|
|
17
|
+
handleDeleteFileClick: (fileId: FileId, event: React.MouseEvent) => void;
|
|
18
|
+
handleFileClick: (fileId: FileId) => void;
|
|
19
|
+
openDirectories: { [path: string]: boolean };
|
|
20
|
+
toggleDirectory: (path: string) => void;
|
|
21
|
+
}) => {
|
|
22
|
+
const { name, file, fileId } = entity as FileTreeFile;
|
|
23
|
+
const { path, children } = entity as FileTree;
|
|
24
|
+
return file ? (
|
|
25
|
+
<FileListing
|
|
26
|
+
fileId={fileId}
|
|
27
|
+
name={name}
|
|
28
|
+
handleRenameFileClick={handleRenameFileClick}
|
|
29
|
+
handleDeleteFileClick={handleDeleteFileClick}
|
|
30
|
+
handleFileClick={handleFileClick}
|
|
31
|
+
/>
|
|
32
|
+
) : (
|
|
33
|
+
<DirectoryListing
|
|
34
|
+
name={name}
|
|
35
|
+
path={path}
|
|
36
|
+
children={children}
|
|
37
|
+
handleRenameFileClick={handleRenameFileClick}
|
|
38
|
+
handleDeleteFileClick={handleDeleteFileClick}
|
|
39
|
+
handleFileClick={handleFileClick}
|
|
40
|
+
openDirectories={openDirectories}
|
|
41
|
+
toggleDirectory={toggleDirectory}
|
|
42
|
+
/>
|
|
43
|
+
);
|
|
44
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { getFileTree } from '../getFileTree';
|
|
3
|
+
import { sortFileTree } from '../sortFileTree';
|
|
4
|
+
import { disableSettings } from '../featureFlags';
|
|
5
|
+
import { Listing } from './Listing';
|
|
6
|
+
import { useOpenDirectories } from './useOpenDirectories';
|
|
7
|
+
|
|
8
|
+
import './styles.scss';
|
|
9
|
+
import { FileId, FileTree, FileTreeFile, Files } from '../../types';
|
|
10
|
+
|
|
11
|
+
export const Sidebar = ({
|
|
12
|
+
createFile,
|
|
13
|
+
files,
|
|
14
|
+
handleRenameFileClick,
|
|
15
|
+
handleDeleteFileClick,
|
|
16
|
+
handleFileClick,
|
|
17
|
+
setIsSettingsOpen,
|
|
18
|
+
isSettingsOpen,
|
|
19
|
+
}: {
|
|
20
|
+
files: Files;
|
|
21
|
+
createFile?: () => void;
|
|
22
|
+
handleRenameFileClick?: (fileId: FileId) => void;
|
|
23
|
+
handleDeleteFileClick?: (fileId: FileId, event: React.MouseEvent) => void;
|
|
24
|
+
handleFileClick?: (fileId: FileId) => void;
|
|
25
|
+
setIsSettingsOpen?: (isSettingsOpen: boolean) => void;
|
|
26
|
+
isSettingsOpen?: boolean;
|
|
27
|
+
}) => {
|
|
28
|
+
const fileTree = useMemo(
|
|
29
|
+
() => (files ? sortFileTree(getFileTree(files)) : null),
|
|
30
|
+
[files]
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const { openDirectories, toggleDirectory } = useOpenDirectories();
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div className="vz-sidebar">
|
|
37
|
+
<div className="files">
|
|
38
|
+
<div className="full-box">
|
|
39
|
+
<div>
|
|
40
|
+
<a className="link-name" href="#">
|
|
41
|
+
Files
|
|
42
|
+
</a>
|
|
43
|
+
</div>
|
|
44
|
+
<div>
|
|
45
|
+
<i
|
|
46
|
+
className="bx bxs-file-plus new-btn"
|
|
47
|
+
style={{ color: '#dbdde1' }}
|
|
48
|
+
onClick={createFile}
|
|
49
|
+
></i>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
{fileTree
|
|
53
|
+
? fileTree.children.map((entity) => {
|
|
54
|
+
const { fileId } = entity as FileTreeFile;
|
|
55
|
+
const { path } = entity as FileTree;
|
|
56
|
+
return (
|
|
57
|
+
<Listing
|
|
58
|
+
entity={entity}
|
|
59
|
+
key={fileId || path}
|
|
60
|
+
handleRenameFileClick={handleRenameFileClick}
|
|
61
|
+
handleDeleteFileClick={handleDeleteFileClick}
|
|
62
|
+
handleFileClick={handleFileClick}
|
|
63
|
+
openDirectories={openDirectories}
|
|
64
|
+
toggleDirectory={toggleDirectory}
|
|
65
|
+
/>
|
|
66
|
+
);
|
|
67
|
+
})
|
|
68
|
+
: null}
|
|
69
|
+
</div>
|
|
70
|
+
{disableSettings ? null : (
|
|
71
|
+
<div
|
|
72
|
+
className="settings"
|
|
73
|
+
onClick={() => setIsSettingsOpen(!isSettingsOpen)}
|
|
74
|
+
>
|
|
75
|
+
Settings
|
|
76
|
+
</div>
|
|
77
|
+
)}
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
.vz-sidebar {
|
|
2
|
+
height: 100%;
|
|
3
|
+
width: 200px;
|
|
4
|
+
background: rgb(54, 65, 89);
|
|
5
|
+
display: flex;
|
|
6
|
+
flex-direction: column;
|
|
7
|
+
justify-content: space-between;
|
|
8
|
+
|
|
9
|
+
.full-box {
|
|
10
|
+
padding: 7px;
|
|
11
|
+
display: flex;
|
|
12
|
+
flex-direction: row;
|
|
13
|
+
align-items: center;
|
|
14
|
+
justify-content: space-between;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.files {
|
|
18
|
+
overflow: auto;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.file-or-directory {
|
|
22
|
+
display: flex;
|
|
23
|
+
flex-direction: row;
|
|
24
|
+
align-items: center;
|
|
25
|
+
justify-content: space-between;
|
|
26
|
+
cursor: pointer;
|
|
27
|
+
margin: 0 10px;
|
|
28
|
+
color: rgb(219, 221, 225);
|
|
29
|
+
border-radius: 5px;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.link-name {
|
|
33
|
+
margin-top: 30px;
|
|
34
|
+
margin-left: 10px;
|
|
35
|
+
color: rgb(154, 160, 172);
|
|
36
|
+
font-size: 12px;
|
|
37
|
+
text-decoration: none;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.new-btn {
|
|
41
|
+
justify-content: right;
|
|
42
|
+
margin-right: 10px;
|
|
43
|
+
cursor: pointer;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.utils {
|
|
47
|
+
display: flex;
|
|
48
|
+
}
|
|
49
|
+
.utilities {
|
|
50
|
+
margin-right: 3px;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.file-or-directory:hover,
|
|
54
|
+
.settings:hover {
|
|
55
|
+
background-color: rgb(74, 84, 105);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.name {
|
|
59
|
+
display: flex;
|
|
60
|
+
text-overflow: ellipsis;
|
|
61
|
+
white-space: nowrap;
|
|
62
|
+
overflow: hidden;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.indentation {
|
|
66
|
+
margin-left: 20px;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.arrow-wrapper {
|
|
70
|
+
margin-right: 4px;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.settings {
|
|
74
|
+
height: 30px;
|
|
75
|
+
color: rgb(219, 221, 225);
|
|
76
|
+
cursor: pointer;
|
|
77
|
+
display: flex;
|
|
78
|
+
align-items: center;
|
|
79
|
+
justify-content: center;
|
|
80
|
+
padding: 14px 0;
|
|
81
|
+
margin: 10px;
|
|
82
|
+
border-radius: 5px;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -17,16 +17,14 @@ import { useState, useCallback } from 'react';
|
|
|
17
17
|
|
|
18
18
|
// Inspired by
|
|
19
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 = (
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const [openDirectories, setOpenDirectories] = useState({});
|
|
20
|
+
export const useOpenDirectories = () => {
|
|
21
|
+
const [openDirectories, setOpenDirectories] = useState<{
|
|
22
|
+
[path: string]: boolean;
|
|
23
|
+
}>({});
|
|
25
24
|
|
|
26
25
|
// TODO consider useReducer to avoid event listener churn
|
|
27
26
|
const toggleDirectory = useCallback(
|
|
28
27
|
(path) => {
|
|
29
|
-
console.log(openDirectories[path], 'here');
|
|
30
28
|
setOpenDirectories(
|
|
31
29
|
Object.assign({}, openDirectories, {
|
|
32
30
|
[path]: !openDirectories[path],
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
// Inspired by
|
|
2
|
+
|
|
3
|
+
import { Files, FileTree } from '../types';
|
|
4
|
+
|
|
2
5
|
// 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' };
|
|
6
|
+
export const getFileTree = (files: Files): FileTree => {
|
|
7
|
+
const tree: FileTree = { name: 'files' };
|
|
5
8
|
for (const fileId of Object.keys(files)) {
|
|
6
9
|
const file = files[fileId];
|
|
7
10
|
const path = file.name.split('/');
|
|
@@ -11,7 +14,7 @@ export const getFileTree = (files) => {
|
|
|
11
14
|
// Walk the path.
|
|
12
15
|
for (let i = 0; i < n - 1; i++) {
|
|
13
16
|
// Search for an existing child.
|
|
14
|
-
let child;
|
|
17
|
+
let child: FileTree | undefined;
|
|
15
18
|
const name = path[i];
|
|
16
19
|
if (node.children) {
|
|
17
20
|
for (let j = 0; j < node.children.length; j++) {
|