vzcode 0.4.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 +47 -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 +29 -19
- package/src/{App.jsx → client/App.tsx} +83 -126
- package/src/client/Sidebar/DirectoryArrowSVG.jsx +5 -0
- package/src/client/Sidebar/DirectoryListing.tsx +76 -0
- package/src/client/Sidebar/FileListing.tsx +42 -0
- package/src/client/Sidebar/Item.jsx +43 -0
- 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.ts +37 -0
- package/src/{diff.js → client/diff.js} +1 -1
- package/src/client/featureFlags.js +3 -0
- package/src/client/getFileTree.ts +52 -0
- package/src/{getOrCreateEditor.js → client/getOrCreateEditor.js} +6 -6
- package/src/{json1PresenceDisplay.js → client/json1PresenceDisplay.js} +0 -1
- package/src/client/settings.jsx +97 -0
- package/src/client/sortFileTree.ts +19 -0
- package/src/client/style.scss +93 -0
- package/src/server/computeInitialDocument.js +69 -0
- package/src/server/featureFlags.js +4 -0
- package/{cli.js → src/server/index.js} +24 -30
- package/src/types.ts +57 -0
- package/dist/assets/index-48dbd5c3.js +0 -89
- package/dist/assets/index-8d2d4b3b.css +0 -1
- package/src/settings.jsx +0 -141
- package/src/style.css +0 -215
- /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
|
@@ -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,17 +1,20 @@
|
|
|
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
|
-
"
|
|
7
|
+
"types": "src/types.ts",
|
|
8
|
+
"bin": "src/server/index.js",
|
|
8
9
|
"files": [
|
|
9
10
|
"dist/**",
|
|
10
11
|
"src/**"
|
|
11
12
|
],
|
|
12
13
|
"scripts": {
|
|
13
|
-
"test": "
|
|
14
|
+
"test": "vitest",
|
|
15
|
+
"test-interactive": "npm run build; cd test/sampleDirectories/kitchenSink; node ../../../src/server/index.js",
|
|
14
16
|
"prettier": "prettier {*.*,**/*.*} --write",
|
|
17
|
+
"tsc": "tsc --noEmit",
|
|
15
18
|
"dev": "vite",
|
|
16
19
|
"build": "vite build",
|
|
17
20
|
"build-release": "vite build --mode release",
|
|
@@ -43,32 +46,39 @@
|
|
|
43
46
|
},
|
|
44
47
|
"homepage": "https://github.com/vizhub-core/vzcode#readme",
|
|
45
48
|
"dependencies": {
|
|
46
|
-
"@codemirror/lang-css": "^6.
|
|
47
|
-
"@codemirror/lang-html": "^6.4.
|
|
48
|
-
"@codemirror/lang-javascript": "^6.1.
|
|
49
|
-
"@codemirror/lang-markdown": "^6.1.
|
|
50
|
-
"@codemirror/state": "^6.2.
|
|
51
|
-
"@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",
|
|
52
55
|
"@teamwork/websocket-json-stream": "^2.0.0",
|
|
56
|
+
"@uiw/codemirror-themes-all": "^4.20.2",
|
|
53
57
|
"codemirror": "^6.0.1",
|
|
54
58
|
"codemirror-ot": "^4.0.0",
|
|
59
|
+
"d3-array": "^3.2.4",
|
|
55
60
|
"diff-match-patch": "^1.0.5",
|
|
61
|
+
"dotenv": "^16.3.0",
|
|
56
62
|
"express": "^4.18.2",
|
|
57
63
|
"json0-ot-diff": "^1.1.2",
|
|
58
|
-
"
|
|
64
|
+
"ngrok": "^4.3.3",
|
|
65
|
+
"open": "^9.1.0",
|
|
59
66
|
"react": "^18.2.0",
|
|
60
|
-
"react-bootstrap": "^2.7.
|
|
67
|
+
"react-bootstrap": "^2.7.4",
|
|
61
68
|
"react-dom": "^18.2.0",
|
|
62
|
-
"react-select": "^5.7.
|
|
63
|
-
"sharedb": "^
|
|
64
|
-
"sharedb-client-browser": "^4.2.
|
|
69
|
+
"react-select": "^5.7.3",
|
|
70
|
+
"sharedb": "^4.0.0",
|
|
71
|
+
"sharedb-client-browser": "^4.2.1",
|
|
65
72
|
"ws": "^8.13.0"
|
|
66
73
|
},
|
|
67
74
|
"devDependencies": {
|
|
68
|
-
"@types/react": "^18.
|
|
69
|
-
"@types/react-dom": "^18.
|
|
70
|
-
"@vitejs/plugin-react": "^
|
|
71
|
-
"prettier": "^2.8.
|
|
72
|
-
"
|
|
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"
|
|
73
83
|
}
|
|
74
84
|
}
|
|
@@ -1,11 +1,14 @@
|
|
|
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
|
-
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 './
|
|
8
|
+
import { Sidebar } from './Sidebar';
|
|
9
|
+
import './style.scss';
|
|
10
|
+
import { oneDark } from '@codemirror/theme-one-dark';
|
|
11
|
+
import { FileId } from '../types';
|
|
9
12
|
|
|
10
13
|
// Register our custom JSON1 OT type that supports presence.
|
|
11
14
|
// See https://github.com/vizhub-core/json1-presence
|
|
@@ -18,10 +21,6 @@ const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
|
|
|
18
21
|
const socket = new WebSocket(wsProtocol + window.location.host + '/ws');
|
|
19
22
|
const connection = new Connection(socket);
|
|
20
23
|
|
|
21
|
-
// A feature flag to disable settings for release, until it's completed.
|
|
22
|
-
// See https://vitejs.dev/guide/env-and-mode.html
|
|
23
|
-
const disableSettings = import.meta.env.VITE_DISABLE_SETTINGS === 'true';
|
|
24
|
-
|
|
25
24
|
function App() {
|
|
26
25
|
// The ShareDB document.
|
|
27
26
|
const [shareDBDoc, setShareDBDoc] = useState(null);
|
|
@@ -39,9 +38,6 @@ function App() {
|
|
|
39
38
|
// updated on each change to decouple rendering from ShareDB.
|
|
40
39
|
const [data, setData] = useState(null);
|
|
41
40
|
|
|
42
|
-
// True if the file menu is open.
|
|
43
|
-
const [isFileMenuOpen, setIsFileMenuOpen] = useState(false);
|
|
44
|
-
|
|
45
41
|
// The id of the currently open file tab.
|
|
46
42
|
const [activeFileId, setActiveFileId] = useState(null);
|
|
47
43
|
|
|
@@ -49,14 +45,10 @@ function App() {
|
|
|
49
45
|
const [tabList, setTabList] = useState([]);
|
|
50
46
|
|
|
51
47
|
// The current theme.
|
|
52
|
-
const [theme, setTheme] = useState(
|
|
53
|
-
|
|
54
|
-
// The currently hovered over file, for showing
|
|
55
|
-
// "utils", meaning buttons for rename and delete.
|
|
56
|
-
const [utils, setUtils] = useState(false);
|
|
48
|
+
const [theme, setTheme] = useState(oneDark);
|
|
57
49
|
|
|
58
50
|
// True to show the settings modal.
|
|
59
|
-
const [
|
|
51
|
+
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
|
60
52
|
|
|
61
53
|
// Set up the connection to ShareDB.
|
|
62
54
|
useEffect(() => {
|
|
@@ -144,15 +136,15 @@ function App() {
|
|
|
144
136
|
);
|
|
145
137
|
|
|
146
138
|
// Called when a file in the sidebar is double-clicked.
|
|
147
|
-
const
|
|
148
|
-
(
|
|
139
|
+
const handleRenameFileClick = useCallback(
|
|
140
|
+
(fileId: FileId) => {
|
|
149
141
|
const newName = prompt('Enter new name');
|
|
150
142
|
if (newName) {
|
|
151
143
|
const currentDocument = shareDBDoc.data;
|
|
152
144
|
const nextDocument = {
|
|
153
145
|
...currentDocument,
|
|
154
|
-
[
|
|
155
|
-
...currentDocument[
|
|
146
|
+
[fileId]: {
|
|
147
|
+
...currentDocument[fileId],
|
|
156
148
|
name: newName,
|
|
157
149
|
},
|
|
158
150
|
};
|
|
@@ -163,22 +155,32 @@ function App() {
|
|
|
163
155
|
);
|
|
164
156
|
|
|
165
157
|
const deleteFile = useCallback(
|
|
166
|
-
(
|
|
158
|
+
(fileId: FileId) => {
|
|
167
159
|
// Close the tab in case it's open.
|
|
168
160
|
// TODO consistently use either "key" or "fileId" naming
|
|
169
|
-
closeTab(
|
|
161
|
+
closeTab(fileId);
|
|
170
162
|
|
|
171
163
|
const currentDocument = shareDBDoc.data;
|
|
172
164
|
const nextDocument = { ...currentDocument };
|
|
173
|
-
delete nextDocument[
|
|
165
|
+
delete nextDocument[fileId];
|
|
174
166
|
shareDBDoc.submitOp(diff(currentDocument, nextDocument));
|
|
175
167
|
},
|
|
176
168
|
[shareDBDoc, closeTab]
|
|
177
169
|
);
|
|
178
170
|
|
|
171
|
+
const handleFileClick = useCallback(
|
|
172
|
+
(fileId: FileId) => {
|
|
173
|
+
setActiveFileId(fileId);
|
|
174
|
+
if (!tabList.includes(fileId)) {
|
|
175
|
+
setTabList([...tabList, fileId]);
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
[tabList]
|
|
179
|
+
);
|
|
180
|
+
|
|
179
181
|
// TODO prompt the user "Are you sure?"
|
|
180
182
|
const handleDeleteFileClick = useCallback(
|
|
181
|
-
(fileId
|
|
183
|
+
(fileId: FileId, event: React.MouseEvent) => {
|
|
182
184
|
// Stop propagation so that the outer listener doesn't fire,
|
|
183
185
|
// which would try to open this file in a tab.
|
|
184
186
|
event.stopPropagation();
|
|
@@ -188,6 +190,7 @@ function App() {
|
|
|
188
190
|
);
|
|
189
191
|
|
|
190
192
|
const createFile = useCallback(() => {
|
|
193
|
+
// TODO better UI, maybe Bootstrap modal? Maybe edit inline?
|
|
191
194
|
const name = prompt('Enter new file name');
|
|
192
195
|
if (!name) return;
|
|
193
196
|
const currentDocument = shareDBDoc.data;
|
|
@@ -202,114 +205,68 @@ function App() {
|
|
|
202
205
|
const tabValid = data && activeFileId;
|
|
203
206
|
|
|
204
207
|
const handleSettingsClose = useCallback(() => {
|
|
205
|
-
|
|
208
|
+
setIsSettingsOpen(false);
|
|
206
209
|
}, []);
|
|
207
210
|
|
|
211
|
+
const fileNameSplit = (fileName) => {
|
|
212
|
+
const split = fileName.split('/');
|
|
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];
|
|
216
|
+
};
|
|
217
|
+
|
|
208
218
|
return (
|
|
209
|
-
|
|
210
|
-
<
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
>
|
|
226
|
-
{tabValid ? data[fileId].name : ''}
|
|
227
|
-
<div
|
|
228
|
-
className={activeFileId ? 'bx bx-x tab-close' : ''}
|
|
229
|
-
onClick={handleCloseTabClick(fileId)}
|
|
230
|
-
></div>
|
|
231
|
-
</div>
|
|
232
|
-
))}
|
|
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
|
+
/>
|
|
233
235
|
</div>
|
|
234
|
-
<div className="
|
|
235
|
-
<div className="
|
|
236
|
-
|
|
237
|
-
<div
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
</div>
|
|
254
|
-
{data
|
|
255
|
-
? Object.keys(data).map((key) => (
|
|
256
|
-
<div className="file" key={key}>
|
|
257
|
-
<div
|
|
258
|
-
className="full-Box"
|
|
259
|
-
onClick={() => {
|
|
260
|
-
setActiveFileId(key);
|
|
261
|
-
if (!tabList.includes(key)) {
|
|
262
|
-
setTabList([...tabList, key]);
|
|
263
|
-
}
|
|
264
|
-
}}
|
|
265
|
-
onMouseEnter={() => {
|
|
266
|
-
setUtils(key);
|
|
267
|
-
}}
|
|
268
|
-
onMouseLeave={() => {
|
|
269
|
-
setUtils(null);
|
|
270
|
-
}}
|
|
271
|
-
>
|
|
272
|
-
<div className={utils === key ? 'hover-name' : 'name'}>
|
|
273
|
-
{data[key].name}
|
|
274
|
-
</div>
|
|
275
|
-
<div className={utils === key ? 'utils' : 'noUtils'}>
|
|
276
|
-
<i
|
|
277
|
-
className="bx bxs-edit utilities"
|
|
278
|
-
style={{ color: '#abdafb' }}
|
|
279
|
-
onClick={() => {
|
|
280
|
-
renameFile(key);
|
|
281
|
-
}}
|
|
282
|
-
></i>
|
|
283
|
-
<i
|
|
284
|
-
className="bx bx-trash"
|
|
285
|
-
style={{ color: '#eb336c' }}
|
|
286
|
-
onClick={handleDeleteFileClick(key)}
|
|
287
|
-
></i>
|
|
288
|
-
</div>
|
|
289
|
-
</div>
|
|
290
|
-
</div>
|
|
291
|
-
))
|
|
292
|
-
: null}
|
|
293
|
-
</div>
|
|
294
|
-
</div>
|
|
295
|
-
{disableSettings ? null : (
|
|
296
|
-
<div className="settings" onClick={() => setSettings(!settings)}>
|
|
297
|
-
Settings
|
|
236
|
+
<div className="right">
|
|
237
|
+
<div className="tab-list">
|
|
238
|
+
{tabList.map((fileId) => (
|
|
239
|
+
<div
|
|
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>
|
|
298
255
|
</div>
|
|
299
|
-
)}
|
|
256
|
+
))}
|
|
300
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}
|
|
301
268
|
</div>
|
|
302
|
-
|
|
303
|
-
<CodeEditor
|
|
304
|
-
className="editor"
|
|
305
|
-
shareDBDoc={shareDBDoc}
|
|
306
|
-
localPresence={localPresence}
|
|
307
|
-
docPresence={docPresence}
|
|
308
|
-
activeFileId={activeFileId}
|
|
309
|
-
theme={theme}
|
|
310
|
-
/>
|
|
311
|
-
) : null}
|
|
312
|
-
</>
|
|
269
|
+
</div>
|
|
313
270
|
);
|
|
314
271
|
}
|
|
315
272
|
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
import { Item } from './Item';
|
|
3
|
+
import { Listing } from './Listing';
|
|
4
|
+
import { DirectoryArrowSVG } from './DirectoryArrowSVG';
|
|
5
|
+
import { FileTree, FileTreeFile } from '../../types';
|
|
6
|
+
|
|
7
|
+
export const DirectoryListing = ({
|
|
8
|
+
name,
|
|
9
|
+
path,
|
|
10
|
+
children,
|
|
11
|
+
handleRenameFileClick,
|
|
12
|
+
handleDeleteFileClick,
|
|
13
|
+
handleFileClick,
|
|
14
|
+
openDirectories,
|
|
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;
|
|
25
|
+
}) => {
|
|
26
|
+
const handleClick = useCallback(() => {
|
|
27
|
+
toggleDirectory(path);
|
|
28
|
+
}, [toggleDirectory]);
|
|
29
|
+
|
|
30
|
+
const handleDeleteClick = useCallback(() => {
|
|
31
|
+
console.log('TODO handleDeleteDirectoryClick');
|
|
32
|
+
}, []);
|
|
33
|
+
|
|
34
|
+
const handleRenameClick = useCallback(() => {
|
|
35
|
+
console.log('TODO handleRenameDirectoryClick');
|
|
36
|
+
}, []);
|
|
37
|
+
|
|
38
|
+
const isOpen = openDirectories[path];
|
|
39
|
+
return (
|
|
40
|
+
<>
|
|
41
|
+
<Item
|
|
42
|
+
name={name}
|
|
43
|
+
handleClick={handleClick}
|
|
44
|
+
handleDeleteClick={handleDeleteClick}
|
|
45
|
+
handleRenameClick={handleRenameClick}
|
|
46
|
+
>
|
|
47
|
+
<div
|
|
48
|
+
className="arrow-wrapper"
|
|
49
|
+
style={{ transform: `rotate(${isOpen ? 90 : 0}deg)` }}
|
|
50
|
+
>
|
|
51
|
+
<DirectoryArrowSVG />
|
|
52
|
+
</div>
|
|
53
|
+
{name}
|
|
54
|
+
</Item>
|
|
55
|
+
{children && isOpen ? (
|
|
56
|
+
<div className="indentation">
|
|
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
|
+
})}
|
|
72
|
+
</div>
|
|
73
|
+
) : null}
|
|
74
|
+
</>
|
|
75
|
+
);
|
|
76
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
import { Item } from './Item';
|
|
3
|
+
import { FileId } from '../../types';
|
|
4
|
+
|
|
5
|
+
export const FileListing = ({
|
|
6
|
+
name,
|
|
7
|
+
fileId,
|
|
8
|
+
handleRenameFileClick,
|
|
9
|
+
handleDeleteFileClick,
|
|
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;
|
|
17
|
+
}) => {
|
|
18
|
+
const handleClick = useCallback(() => {
|
|
19
|
+
handleFileClick(fileId);
|
|
20
|
+
}, [fileId, handleFileClick]);
|
|
21
|
+
|
|
22
|
+
const handleDeleteClick = useCallback(
|
|
23
|
+
(event) => {
|
|
24
|
+
handleDeleteFileClick(fileId, event);
|
|
25
|
+
},
|
|
26
|
+
[fileId]
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const handleRenameClick = useCallback(() => {
|
|
30
|
+
handleRenameFileClick(fileId);
|
|
31
|
+
}, [fileId]);
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<Item
|
|
35
|
+
handleClick={handleClick}
|
|
36
|
+
handleDeleteClick={handleDeleteClick}
|
|
37
|
+
handleRenameClick={handleRenameClick}
|
|
38
|
+
>
|
|
39
|
+
{name}
|
|
40
|
+
</Item>
|
|
41
|
+
);
|
|
42
|
+
};
|
|
@@ -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,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
|
+
};
|