vzcode 0.25.0 → 0.27.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/assets/index-5f9f376e.css +1 -0
- package/dist/assets/index-70b6d947.js +111 -0
- package/dist/index.html +2 -6
- package/package.json +7 -7
- package/src/client/App.tsx +110 -247
- package/src/client/CodeEditor/getOrCreateEditor.ts +6 -1
- package/src/client/CodeEditor/index.tsx +94 -16
- package/src/client/CodeEditor/{json1PresenceBroadcast.js → json1PresenceBroadcast.ts} +9 -3
- package/src/client/CodeEditor/{json1PresenceDisplay.js → json1PresenceDisplay.ts} +9 -48
- package/src/client/CodeEditor/style.scss +5 -1
- package/src/client/CodeErrorOverlay/index.tsx +27 -0
- package/src/client/CodeErrorOverlay/style.scss +30 -0
- package/src/client/PresenceNotifications/PresenceNotifications.test.ts +134 -0
- package/src/client/PresenceNotifications/index.tsx +119 -106
- package/src/client/SplitPaneResizeContext.tsx +12 -7
- package/src/client/TabList/Tab.tsx +70 -0
- package/src/client/TabList/index.tsx +28 -69
- package/src/client/TabList/style.scss +26 -9
- package/src/client/{Settings.tsx → VZSettings.tsx} +34 -21
- package/src/client/{Sidebar/ConfirmationBox.tsx → VZSidebar/DeleteConfirmationModal.tsx} +17 -14
- package/src/client/{Sidebar → VZSidebar}/DirectoryListing.tsx +27 -26
- package/src/client/{Sidebar → VZSidebar}/FileListing.tsx +19 -19
- package/src/client/VZSidebar/Item.tsx +179 -0
- package/src/client/{Sidebar → VZSidebar}/Listing.tsx +20 -14
- package/src/client/{Sidebar → VZSidebar}/index.tsx +48 -30
- package/src/client/{Sidebar → VZSidebar}/styles.scss +17 -12
- package/src/client/bootstrap.ts +3 -0
- package/src/client/style.scss +8 -2
- package/src/client/useActions.ts +93 -0
- package/src/client/useEditorCache.ts +2 -1
- package/src/client/useFileCRUD.ts +103 -0
- package/src/client/useOpenDirectories.ts +25 -7
- package/src/client/useSubmitOperation.ts +25 -0
- package/src/client/usernameLocalStorage.ts +43 -0
- package/src/client/vzReducer/closeTabReducer.test.ts +177 -0
- package/src/client/vzReducer/closeTabsReducer.ts +68 -0
- package/src/client/vzReducer/createInitialState.ts +19 -0
- package/src/client/vzReducer/editorNoLongerWantsFocusReducer.ts +9 -0
- package/src/client/vzReducer/index.ts +107 -0
- package/src/client/vzReducer/openTabReducer.test.ts +110 -0
- package/src/client/vzReducer/openTabReducer.ts +57 -0
- package/src/client/vzReducer/setActiveFileIdReducer.ts +9 -0
- package/src/client/vzReducer/setIsSettingsOpenReducer.ts +9 -0
- package/src/client/vzReducer/setThemeReducer.ts +9 -0
- package/src/client/vzReducer/setUsernameReducer.ts +9 -0
- package/src/index.ts +40 -0
- package/src/server/index.js +13 -2
- package/src/types.ts +12 -5
- package/dist/assets/index-8628ebab.js +0 -111
- package/dist/assets/index-cf19de3b.css +0 -1
- package/src/client/PrettierErrorOverlay/index.tsx +0 -9
- package/src/client/PrettierErrorOverlay/style.scss +0 -9
- package/src/client/Sidebar/Item.tsx +0 -130
- package/src/client/reducer.ts +0 -119
- /package/src/client/{Sidebar → VZSidebar}/DirectoryArrowSVG.jsx +0 -0
|
@@ -1,145 +1,158 @@
|
|
|
1
1
|
import { useState, useEffect, useRef } from 'react';
|
|
2
|
+
import { PresenceId, Username } from '../../types';
|
|
2
3
|
import './style.scss';
|
|
3
4
|
|
|
4
|
-
type PresenceId = string;
|
|
5
|
-
|
|
6
|
-
// A presence notification is a message that is displayed when a user joins or leaves a session.
|
|
7
|
-
// It has the following properties:
|
|
8
|
-
// - `user` is the username of the user who joined or left the session.
|
|
9
|
-
// - `join` is a boolean value that indicates whether the user joined or left the session.
|
|
10
|
-
// - `true` means user has joined the session.
|
|
11
|
-
// - `false` means user has left the session.
|
|
12
5
|
type PresenceNotification = {
|
|
13
|
-
user:
|
|
6
|
+
user: Username;
|
|
14
7
|
join: boolean;
|
|
15
8
|
presenceId: PresenceId;
|
|
16
9
|
};
|
|
17
10
|
|
|
11
|
+
export const extractTimestampFromId = (
|
|
12
|
+
id: string,
|
|
13
|
+
): number => {
|
|
14
|
+
const [timestampPart] = id.split('-');
|
|
15
|
+
return parseInt(timestampPart, 36);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const shouldShowNotification = (
|
|
19
|
+
remotePresenceId: PresenceId,
|
|
20
|
+
localPresenceId: PresenceId,
|
|
21
|
+
): boolean => {
|
|
22
|
+
const remotePresenceTimestamp = extractTimestampFromId(
|
|
23
|
+
remotePresenceId,
|
|
24
|
+
);
|
|
25
|
+
const localPresenceTimestamp =
|
|
26
|
+
extractTimestampFromId(localPresenceId);
|
|
27
|
+
return remotePresenceTimestamp > localPresenceTimestamp;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const handleJoin = (
|
|
31
|
+
presenceId: PresenceId,
|
|
32
|
+
username: Username,
|
|
33
|
+
alreadyJoined: React.MutableRefObject<
|
|
34
|
+
Map<PresenceId, Username>
|
|
35
|
+
>,
|
|
36
|
+
setPresenceNotifications: any,
|
|
37
|
+
) => {
|
|
38
|
+
if (!alreadyJoined.current.has(presenceId)) {
|
|
39
|
+
alreadyJoined.current.set(presenceId, username);
|
|
40
|
+
setPresenceNotifications(
|
|
41
|
+
(prev: PresenceNotification[]) => [
|
|
42
|
+
...prev,
|
|
43
|
+
{ user: username, join: true, presenceId },
|
|
44
|
+
],
|
|
45
|
+
);
|
|
46
|
+
} else {
|
|
47
|
+
alreadyJoined.current.set(presenceId, username); // Update in case username changed
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const handleLeave = (
|
|
52
|
+
presenceId: PresenceId,
|
|
53
|
+
alreadyJoined: React.MutableRefObject<
|
|
54
|
+
Map<PresenceId, Username>
|
|
55
|
+
>,
|
|
56
|
+
setPresenceNotifications: any,
|
|
57
|
+
) => {
|
|
58
|
+
const user = alreadyJoined.current.get(presenceId);
|
|
59
|
+
if (user) {
|
|
60
|
+
alreadyJoined.current.delete(presenceId);
|
|
61
|
+
setPresenceNotifications(
|
|
62
|
+
(prev: PresenceNotification[]) => [
|
|
63
|
+
...prev,
|
|
64
|
+
{ user, join: false, presenceId },
|
|
65
|
+
],
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
18
70
|
export const PresenceNotifications = ({
|
|
19
71
|
docPresence,
|
|
72
|
+
localPresence,
|
|
20
73
|
}: {
|
|
21
74
|
docPresence?: any;
|
|
75
|
+
localPresence?: any;
|
|
22
76
|
}) => {
|
|
77
|
+
// `presenceNotifications`:
|
|
78
|
+
// - State variable containing a list of user presence notifications.
|
|
79
|
+
// - Each notification indicates if a user has joined or left a session.
|
|
23
80
|
const [presenceNotifications, setPresenceNotifications] =
|
|
24
|
-
useState<
|
|
25
|
-
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
|
|
81
|
+
useState<PresenceNotification[]>([]);
|
|
82
|
+
|
|
83
|
+
// `alreadyJoined`:
|
|
84
|
+
// - Ref (persistent across re-renders) containing a Map.
|
|
85
|
+
// - Map's key: `PresenceId` (unique identifier for user's presence).
|
|
86
|
+
// - Map's value: `Username` of the associated user.
|
|
87
|
+
// - Tracks users who have joined the session and their possible username changes.
|
|
88
|
+
const alreadyJoined = useRef<Map<PresenceId, Username>>(
|
|
89
|
+
new Map(),
|
|
31
90
|
);
|
|
32
91
|
|
|
33
92
|
useEffect(() => {
|
|
34
|
-
// console.log('useEffect triggered.');
|
|
35
93
|
if (docPresence) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
// Attempted solutions:
|
|
42
|
-
// - create an array called presenceIds that keeps track of the presences that join and leave
|
|
43
|
-
// - if presenceId is in presenceIds: local user was already alerted that a remote user joined
|
|
44
|
-
// - if presenceId is not in presenceIds: local user needs to be notified about a new user
|
|
45
|
-
// - problems:
|
|
46
|
-
// - the array is always empty inside useEffect (array works outside of useEffect)
|
|
47
|
-
// - causes check to fail
|
|
48
|
-
// - check docPresence.remotePresences, which keeps track of all the remote presences on the doc
|
|
49
|
-
// - see https://share.github.io/sharedb/api/presence
|
|
50
|
-
// - if presenceId is in docPresence.remotePresences: local user was already alerted that a remote user joined
|
|
51
|
-
// - if presenceId is not in docPresence.remotePresences: local user needs to be notified about a new user
|
|
52
|
-
// - problems:
|
|
53
|
-
// - useEffects is called when docPresence changes (in some cases, it's when the remote presence changes)
|
|
54
|
-
// - remotePresences already has the remote presence in the array, so check fails
|
|
55
|
-
// - could add another property to presence (true: already notified, false: need to be notified)
|
|
56
|
-
// - doesn't seem like an ideal solution
|
|
57
|
-
|
|
58
|
-
// console.log('docPresence:', docPresence);
|
|
59
|
-
// console.log('docPresence remotePresences:', docPresence.remotePresences);
|
|
60
|
-
// console.log('check for presence:', docPresence.remotePresences[presenceId]);
|
|
61
|
-
|
|
62
|
-
// TODO figure out how to get username from `presenceId`.
|
|
63
|
-
const user = 'someone';
|
|
64
|
-
|
|
65
|
-
// `true` means user has joined the session.
|
|
66
|
-
// `false` means user has left the session.
|
|
94
|
+
const handleReceive = (
|
|
95
|
+
presenceId: PresenceId,
|
|
96
|
+
update: any,
|
|
97
|
+
) => {
|
|
67
98
|
const join = update !== null;
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
99
|
+
if (
|
|
100
|
+
shouldShowNotification(
|
|
101
|
+
presenceId,
|
|
102
|
+
localPresence.presenceId,
|
|
103
|
+
)
|
|
104
|
+
) {
|
|
105
|
+
if (join) {
|
|
106
|
+
const username =
|
|
107
|
+
docPresence.remotePresences[presenceId]
|
|
108
|
+
?.username;
|
|
109
|
+
handleJoin(
|
|
77
110
|
presenceId,
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
111
|
+
username,
|
|
112
|
+
alreadyJoined,
|
|
113
|
+
setPresenceNotifications,
|
|
114
|
+
);
|
|
115
|
+
} else {
|
|
116
|
+
handleLeave(
|
|
81
117
|
presenceId,
|
|
118
|
+
alreadyJoined,
|
|
119
|
+
setPresenceNotifications,
|
|
82
120
|
);
|
|
83
|
-
setPresenceNotifications((prev) => [
|
|
84
|
-
...prev,
|
|
85
|
-
{ user, join, presenceId },
|
|
86
|
-
]);
|
|
87
|
-
|
|
88
|
-
// Remove it after 5 seconds.
|
|
89
|
-
setTimeout(() => {
|
|
90
|
-
setPresenceNotifications((prev) =>
|
|
91
|
-
prev.filter(
|
|
92
|
-
(notification) =>
|
|
93
|
-
notification.presenceId !==
|
|
94
|
-
presenceId &&
|
|
95
|
-
notification.join !== join,
|
|
96
|
-
),
|
|
97
|
-
);
|
|
98
|
-
}, 5000);
|
|
99
121
|
}
|
|
100
|
-
} else if (!join) {
|
|
101
|
-
setPresenceNotifications((prev) => [
|
|
102
|
-
...prev,
|
|
103
|
-
{ user, join, presenceId },
|
|
104
|
-
]);
|
|
105
122
|
|
|
106
|
-
|
|
107
|
-
// TODO clean up this duplication.
|
|
108
|
-
setTimeout(() => {
|
|
123
|
+
const timeoutId = setTimeout(() => {
|
|
109
124
|
setPresenceNotifications((prev) =>
|
|
110
|
-
prev.
|
|
111
|
-
(notification) =>
|
|
112
|
-
notification.presenceId !== presenceId &&
|
|
113
|
-
notification.join !== join,
|
|
114
|
-
),
|
|
125
|
+
prev.slice(1),
|
|
115
126
|
);
|
|
116
127
|
}, 5000);
|
|
128
|
+
|
|
129
|
+
return () => {
|
|
130
|
+
clearTimeout(timeoutId);
|
|
131
|
+
};
|
|
117
132
|
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}, [docPresence]);
|
|
133
|
+
};
|
|
121
134
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
// presenceIds,
|
|
129
|
-
// );
|
|
135
|
+
docPresence.on('receive', handleReceive);
|
|
136
|
+
return () => {
|
|
137
|
+
docPresence.off('receive', handleReceive);
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}, [docPresence, localPresence]);
|
|
130
141
|
|
|
131
142
|
return presenceNotifications.length > 0 ? (
|
|
132
143
|
<div className="vz-notification">
|
|
133
|
-
{presenceNotifications.map(
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
144
|
+
{presenceNotifications.map(
|
|
145
|
+
({ user, join, presenceId }) => (
|
|
146
|
+
<div
|
|
147
|
+
key={presenceId}
|
|
148
|
+
className="vz-notification-item"
|
|
149
|
+
>
|
|
137
150
|
{join
|
|
138
151
|
? `${user} has joined the session.`
|
|
139
152
|
: `${user} has left the session.`}
|
|
140
153
|
</div>
|
|
141
|
-
)
|
|
142
|
-
|
|
154
|
+
),
|
|
155
|
+
)}
|
|
143
156
|
</div>
|
|
144
157
|
) : null;
|
|
145
158
|
};
|
|
@@ -6,13 +6,6 @@ import {
|
|
|
6
6
|
useEffect,
|
|
7
7
|
} from 'react';
|
|
8
8
|
|
|
9
|
-
export const SplitPaneResizeContext = createContext<{
|
|
10
|
-
codeEditorWidth: number;
|
|
11
|
-
moveSplitPane: (a: number) => void;
|
|
12
|
-
isDragging: boolean;
|
|
13
|
-
setIsDragging: (a: boolean) => void;
|
|
14
|
-
}>(null);
|
|
15
|
-
|
|
16
9
|
// The amount of time to wait idle before writing to localStorage.
|
|
17
10
|
// This is to avoid writing to localStorage on every resize event.
|
|
18
11
|
// MS = milliseconds
|
|
@@ -38,6 +31,18 @@ if (typeof window !== 'undefined') {
|
|
|
38
31
|
// If we're not in the browser, use the default initial width.
|
|
39
32
|
}
|
|
40
33
|
|
|
34
|
+
export const SplitPaneResizeContext = createContext<{
|
|
35
|
+
codeEditorWidth: number;
|
|
36
|
+
moveSplitPane: (a: number) => void;
|
|
37
|
+
isDragging: boolean;
|
|
38
|
+
setIsDragging: (a: boolean) => void;
|
|
39
|
+
}>({
|
|
40
|
+
codeEditorWidth: initialWidth,
|
|
41
|
+
moveSplitPane: () => {},
|
|
42
|
+
isDragging: false,
|
|
43
|
+
setIsDragging: () => {},
|
|
44
|
+
});
|
|
45
|
+
|
|
41
46
|
const add = (a: number, b: number) => a + b;
|
|
42
47
|
|
|
43
48
|
export const SplitPaneResizeProvider = ({ children }) => {
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { useCallback, useMemo } from 'react';
|
|
2
|
+
import type { FileId } from '../../types';
|
|
3
|
+
import type { TabState } from '../vzReducer';
|
|
4
|
+
|
|
5
|
+
// Supports adding the file's containing folder to the tab name
|
|
6
|
+
const fileNameSplit = (fileName: string) => {
|
|
7
|
+
const split = fileName.split('/');
|
|
8
|
+
if (split.length === 1) return split[split.length - 1];
|
|
9
|
+
return (
|
|
10
|
+
split[split.length - 2] + '/' + split[split.length - 1]
|
|
11
|
+
);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const Tab = ({
|
|
15
|
+
fileId,
|
|
16
|
+
isTransient,
|
|
17
|
+
isActive,
|
|
18
|
+
setActiveFileId,
|
|
19
|
+
openTab,
|
|
20
|
+
closeTabs,
|
|
21
|
+
fileName,
|
|
22
|
+
}: {
|
|
23
|
+
fileId: FileId;
|
|
24
|
+
isTransient: boolean;
|
|
25
|
+
isActive: boolean;
|
|
26
|
+
setActiveFileId: (fileId: FileId) => void;
|
|
27
|
+
openTab: (tabState: TabState) => void;
|
|
28
|
+
closeTabs: (fileIds: FileId[]) => void;
|
|
29
|
+
fileName: string;
|
|
30
|
+
}) => {
|
|
31
|
+
const handleCloseClick = useCallback(
|
|
32
|
+
(event: React.MouseEvent) => {
|
|
33
|
+
// Stop propagation so that the outer listener doesn't fire.
|
|
34
|
+
event.stopPropagation();
|
|
35
|
+
|
|
36
|
+
closeTabs([fileId]);
|
|
37
|
+
},
|
|
38
|
+
[closeTabs, fileId],
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const tabName = useMemo(
|
|
42
|
+
() => fileNameSplit(fileName),
|
|
43
|
+
[fileName],
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const handleClick = useCallback(() => {
|
|
47
|
+
setActiveFileId(fileId);
|
|
48
|
+
}, [fileId, setActiveFileId]);
|
|
49
|
+
|
|
50
|
+
// Double-clicking a transient tab makes it a persistent tab.
|
|
51
|
+
const handleDoubleClick = useCallback(() => {
|
|
52
|
+
openTab({ fileId, isTransient: false });
|
|
53
|
+
}, [fileId, openTab]);
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div
|
|
57
|
+
className={`tab user-select-none ${
|
|
58
|
+
isActive ? 'active' : ''
|
|
59
|
+
} ${isTransient ? 'transient' : ''}`}
|
|
60
|
+
onClick={handleClick}
|
|
61
|
+
onDoubleClick={handleDoubleClick}
|
|
62
|
+
>
|
|
63
|
+
{tabName}
|
|
64
|
+
<div
|
|
65
|
+
className={'bx bx-x tab-close'}
|
|
66
|
+
onClick={handleCloseClick}
|
|
67
|
+
></div>
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
};
|
|
@@ -1,89 +1,45 @@
|
|
|
1
|
-
import { useCallback,
|
|
2
|
-
import {
|
|
1
|
+
import { useCallback, useEffect } from 'react';
|
|
2
|
+
import type { TabState } from '../vzReducer';
|
|
3
|
+
import type { FileId, Files } from '../../types';
|
|
4
|
+
import { Tab } from './Tab';
|
|
3
5
|
import './style.scss';
|
|
4
6
|
|
|
5
|
-
// Supports adding the file's containing folder to the tab name
|
|
6
|
-
const fileNameSplit = (fileName) => {
|
|
7
|
-
const split = fileName.split('/');
|
|
8
|
-
if (split.length === 1) return split[split.length - 1];
|
|
9
|
-
return (
|
|
10
|
-
split[split.length - 2] + '/' + split[split.length - 1]
|
|
11
|
-
);
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
const Tab = ({
|
|
15
|
-
fileId,
|
|
16
|
-
isActive,
|
|
17
|
-
setActiveFileId,
|
|
18
|
-
closeTab,
|
|
19
|
-
fileName,
|
|
20
|
-
}: {
|
|
21
|
-
fileId: FileId;
|
|
22
|
-
isActive: boolean;
|
|
23
|
-
setActiveFileId: (fileId: FileId) => void;
|
|
24
|
-
closeTab: (fileId: FileId) => void;
|
|
25
|
-
fileName: string;
|
|
26
|
-
}) => {
|
|
27
|
-
const handleCloseClick = useCallback(
|
|
28
|
-
(event: React.MouseEvent) => {
|
|
29
|
-
// Stop propagation so that the outer listener doesn't fire.
|
|
30
|
-
event.stopPropagation();
|
|
31
|
-
|
|
32
|
-
closeTab(fileId);
|
|
33
|
-
},
|
|
34
|
-
[closeTab, fileId],
|
|
35
|
-
);
|
|
36
|
-
|
|
37
|
-
const tabName = useMemo(
|
|
38
|
-
() => fileNameSplit(fileName),
|
|
39
|
-
[fileName],
|
|
40
|
-
);
|
|
41
|
-
|
|
42
|
-
return (
|
|
43
|
-
<div
|
|
44
|
-
className={isActive ? 'tab active' : 'tab'}
|
|
45
|
-
onClick={() => {
|
|
46
|
-
setActiveFileId(fileId);
|
|
47
|
-
}}
|
|
48
|
-
>
|
|
49
|
-
{tabName}
|
|
50
|
-
<div
|
|
51
|
-
className={isActive ? 'bx bx-x tab-close' : ''}
|
|
52
|
-
onClick={handleCloseClick}
|
|
53
|
-
></div>
|
|
54
|
-
</div>
|
|
55
|
-
);
|
|
56
|
-
};
|
|
57
|
-
|
|
58
7
|
// Displays the list of tabs above the code editor.
|
|
59
8
|
export const TabList = ({
|
|
60
9
|
files,
|
|
61
10
|
tabList,
|
|
62
11
|
activeFileId,
|
|
63
12
|
setActiveFileId,
|
|
64
|
-
|
|
13
|
+
openTab,
|
|
14
|
+
closeTabs,
|
|
15
|
+
createFile,
|
|
65
16
|
}: {
|
|
66
17
|
files: Files;
|
|
67
|
-
tabList:
|
|
18
|
+
tabList: Array<TabState>;
|
|
68
19
|
activeFileId: FileId;
|
|
69
20
|
setActiveFileId: (fileId: FileId) => void;
|
|
70
|
-
|
|
21
|
+
openTab: (tabState: TabState) => void;
|
|
22
|
+
closeTabs: (fileIds: FileId[]) => void;
|
|
23
|
+
createFile: () => void;
|
|
71
24
|
}) => {
|
|
72
|
-
//
|
|
25
|
+
// Close the active tab on alt+w
|
|
73
26
|
const handleKeyPress = useCallback(
|
|
74
27
|
(event: { altKey: boolean; key: string }) => {
|
|
75
28
|
if (event.altKey == true) {
|
|
76
29
|
if (event.key == 'w') {
|
|
77
|
-
|
|
30
|
+
closeTabs([activeFileId]);
|
|
31
|
+
}
|
|
32
|
+
if (event.key == 'n') {
|
|
33
|
+
createFile();
|
|
78
34
|
}
|
|
79
35
|
}
|
|
80
36
|
},
|
|
81
|
-
[
|
|
37
|
+
[createFile, closeTabs, activeFileId],
|
|
82
38
|
);
|
|
39
|
+
|
|
40
|
+
// Add the global keydown event listener
|
|
83
41
|
useEffect(() => {
|
|
84
|
-
// attach the event listener
|
|
85
42
|
document.addEventListener('keydown', handleKeyPress);
|
|
86
|
-
// remove the event listener
|
|
87
43
|
return () => {
|
|
88
44
|
document.removeEventListener(
|
|
89
45
|
'keydown',
|
|
@@ -91,17 +47,20 @@ export const TabList = ({
|
|
|
91
47
|
);
|
|
92
48
|
};
|
|
93
49
|
}, [handleKeyPress]);
|
|
50
|
+
|
|
94
51
|
return (
|
|
95
52
|
<div className="vz-tab-list">
|
|
96
53
|
{files &&
|
|
97
|
-
tabList.map((
|
|
54
|
+
tabList.map((tabState: TabState) => (
|
|
98
55
|
<Tab
|
|
99
|
-
key={fileId}
|
|
100
|
-
fileId={fileId}
|
|
101
|
-
|
|
56
|
+
key={tabState.fileId}
|
|
57
|
+
fileId={tabState.fileId}
|
|
58
|
+
isTransient={tabState.isTransient}
|
|
59
|
+
isActive={tabState.fileId === activeFileId}
|
|
102
60
|
setActiveFileId={setActiveFileId}
|
|
103
|
-
|
|
104
|
-
|
|
61
|
+
openTab={openTab}
|
|
62
|
+
closeTabs={closeTabs}
|
|
63
|
+
fileName={files[tabState.fileId].name}
|
|
105
64
|
/>
|
|
106
65
|
))}
|
|
107
66
|
</div>
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
.vz-tab-list {
|
|
2
2
|
display: flex;
|
|
3
|
-
background-color:
|
|
3
|
+
background-color: #33415b;
|
|
4
4
|
// When the tab list becomes too large, a horizontal scroll bar appears.
|
|
5
5
|
overflow-x: auto;
|
|
6
6
|
|
|
7
7
|
.tab {
|
|
8
|
-
background:
|
|
8
|
+
// background: #47546b;
|
|
9
9
|
color: #fff;
|
|
10
10
|
font-family: var(--vzcode-font-family);
|
|
11
11
|
align-items: center;
|
|
12
12
|
display: flex;
|
|
13
13
|
padding-top: 2px;
|
|
14
|
-
padding-left:
|
|
15
|
-
padding-right:
|
|
16
|
-
border-top-left-radius:
|
|
17
|
-
border-top-right-radius:
|
|
14
|
+
padding-left: 8px;
|
|
15
|
+
padding-right: 5px;
|
|
16
|
+
border-top-left-radius: 12px;
|
|
17
|
+
border-top-right-radius: 12px;
|
|
18
18
|
margin-top: 5px;
|
|
19
19
|
margin-left: 2px;
|
|
20
20
|
margin-right: 2px;
|
|
@@ -22,15 +22,32 @@
|
|
|
22
22
|
// Make sure text doesn't wrap within tabs.
|
|
23
23
|
white-space: nowrap;
|
|
24
24
|
|
|
25
|
+
// TODO unify with styles in code error overlay dismiss button
|
|
25
26
|
.tab-close {
|
|
26
27
|
margin-left: 5px;
|
|
28
|
+
margin-bottom: 2px;
|
|
27
29
|
}
|
|
28
30
|
.tab-close:hover {
|
|
29
|
-
border-radius:
|
|
30
|
-
background-color:
|
|
31
|
+
border-radius: 50%;
|
|
32
|
+
background-color: rgba(255, 255, 255, 0.2);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// No left margin for first child
|
|
36
|
+
&:first-child {
|
|
37
|
+
margin-left: 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// on hover
|
|
41
|
+
&:hover {
|
|
42
|
+
background-color: #47546b;
|
|
31
43
|
}
|
|
32
44
|
}
|
|
45
|
+
|
|
33
46
|
.tab.active {
|
|
34
|
-
background-color:
|
|
47
|
+
background-color: #202e46;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.tab.transient {
|
|
51
|
+
font-style: italic;
|
|
35
52
|
}
|
|
36
53
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useCallback, useRef } from 'react';
|
|
2
2
|
import { Button, Modal, Form } from './bootstrap';
|
|
3
3
|
import { ThemeLabel, themes } from './themes';
|
|
4
|
+
import { Username } from '../types';
|
|
4
5
|
|
|
5
6
|
// const saveTimes = [
|
|
6
7
|
// { value: 1, label: '1 second' },
|
|
@@ -9,18 +10,25 @@ import { ThemeLabel, themes } from './themes';
|
|
|
9
10
|
// { value: 30, label: '30 seconds' },
|
|
10
11
|
// ];
|
|
11
12
|
|
|
12
|
-
export
|
|
13
|
-
|
|
14
|
-
export const Settings = ({
|
|
13
|
+
export const VZSettings = ({
|
|
15
14
|
show,
|
|
16
15
|
onClose,
|
|
17
|
-
setTheme,
|
|
18
16
|
theme,
|
|
17
|
+
setTheme,
|
|
18
|
+
username,
|
|
19
|
+
setUsername,
|
|
20
|
+
|
|
21
|
+
enableUsernameField = true,
|
|
19
22
|
}: {
|
|
20
23
|
show: boolean;
|
|
21
24
|
onClose: () => void;
|
|
22
|
-
setTheme: (theme: ThemeLabel) => void;
|
|
23
25
|
theme: ThemeLabel;
|
|
26
|
+
setTheme: (theme: ThemeLabel) => void;
|
|
27
|
+
username: Username;
|
|
28
|
+
setUsername: (username: Username) => void;
|
|
29
|
+
|
|
30
|
+
// Feature flag to enable/disable username field
|
|
31
|
+
enableUsernameField?: boolean;
|
|
24
32
|
}) => {
|
|
25
33
|
const handleThemeChange = useCallback((event) => {
|
|
26
34
|
const selectedValue = event.target.value;
|
|
@@ -44,10 +52,10 @@ export const Settings = ({
|
|
|
44
52
|
// );
|
|
45
53
|
|
|
46
54
|
const usernameRef = useRef<HTMLInputElement>(null);
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
};
|
|
55
|
+
|
|
56
|
+
const handleUsernameChange = useCallback(() => {
|
|
57
|
+
setUsername(usernameRef.current?.value || '');
|
|
58
|
+
}, [setUsername]);
|
|
51
59
|
|
|
52
60
|
return show ? (
|
|
53
61
|
<Modal
|
|
@@ -60,18 +68,23 @@ export const Settings = ({
|
|
|
60
68
|
<Modal.Title>Settings</Modal.Title>
|
|
61
69
|
</Modal.Header>
|
|
62
70
|
<Modal.Body>
|
|
63
|
-
|
|
64
|
-
<Form.
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
71
|
+
{enableUsernameField ? (
|
|
72
|
+
<Form.Group className="mb-3" controlId="formFork">
|
|
73
|
+
<Form.Label>Username</Form.Label>
|
|
74
|
+
<Form.Control
|
|
75
|
+
type="text"
|
|
76
|
+
ref={usernameRef}
|
|
77
|
+
onChange={handleUsernameChange}
|
|
78
|
+
placeholder="Enter username"
|
|
79
|
+
value={username}
|
|
80
|
+
/>
|
|
81
|
+
<Form.Text className="text-muted">
|
|
82
|
+
Enter a username to be displayed on your
|
|
83
|
+
cursor
|
|
84
|
+
</Form.Text>
|
|
85
|
+
</Form.Group>
|
|
86
|
+
) : null}
|
|
87
|
+
|
|
75
88
|
<Form.Group className="mb-3" controlId="formFork">
|
|
76
89
|
<Form.Label>Theme</Form.Label>
|
|
77
90
|
|