vzcode 0.26.0 → 0.28.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 +1 -1
- package/dist/assets/index-a9585cbc.js +111 -0
- package/dist/assets/index-d30c9f1b.css +1 -0
- package/dist/index.html +2 -2
- package/package.json +6 -4
- package/src/client/AIAssist.ts +46 -0
- package/src/client/App.tsx +86 -43
- package/src/client/CodeEditor/getOrCreateEditor.ts +22 -2
- package/src/client/CodeEditor/index.tsx +75 -12
- package/src/client/CodeEditor/json1PresenceBroadcast.ts +6 -2
- package/src/client/CodeEditor/json1PresenceDisplay.ts +0 -45
- package/src/client/CodeEditor/style.scss +5 -1
- package/src/client/CodeErrorOverlay/index.tsx +37 -0
- package/src/client/CodeErrorOverlay/style.scss +41 -0
- package/src/client/PresenceNotifications/PresenceNotifications.test.ts +134 -0
- package/src/client/PresenceNotifications/index.tsx +118 -103
- package/src/client/SplitPaneResizeContext.tsx +12 -7
- package/src/client/TabList/Tab.tsx +20 -6
- package/src/client/TabList/index.tsx +18 -8
- package/src/client/TabList/style.scss +5 -0
- package/src/client/VZSettings.tsx +33 -20
- package/src/client/VZSidebar/{ConfirmationBox.tsx → DeleteConfirmationModal.tsx} +17 -14
- package/src/client/VZSidebar/DirectoryListing.tsx +27 -26
- package/src/client/VZSidebar/FileListing.tsx +16 -19
- package/src/client/VZSidebar/Item.tsx +83 -37
- package/src/client/VZSidebar/Listing.tsx +17 -14
- package/src/client/VZSidebar/index.tsx +87 -45
- package/src/client/VZSidebar/styles.scss +21 -1
- package/src/client/style.scss +3 -0
- package/src/client/useActions.ts +25 -6
- package/src/client/useEditorCache.ts +2 -1
- package/src/client/useFileCRUD.ts +29 -27
- 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 +9 -3
- package/src/server/handleAIAssist.js +69 -0
- package/src/server/index.js +9 -0
- package/src/types.ts +12 -5
- package/dist/assets/index-284b7339.js +0 -111
- package/dist/assets/index-e160f758.css +0 -1
- package/src/client/PrettierErrorOverlay/index.tsx +0 -9
- package/src/client/PrettierErrorOverlay/style.scss +0 -9
- package/src/client/vzReducer.ts +0 -118
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
extractTimestampFromId,
|
|
4
|
+
shouldShowNotification,
|
|
5
|
+
handleJoin,
|
|
6
|
+
handleLeave,
|
|
7
|
+
} from '.';
|
|
8
|
+
|
|
9
|
+
describe('extractTimestampFromId', () => {
|
|
10
|
+
it('Extracts a timestamp from a given ID', () => {
|
|
11
|
+
const id = 'abcd-xyz';
|
|
12
|
+
const timestamp = extractTimestampFromId(id);
|
|
13
|
+
expect(timestamp).toBe(parseInt('abcd', 36));
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe('shouldShowNotification', () => {
|
|
18
|
+
it('Returns true if remote presence timestamp is greater than local', () => {
|
|
19
|
+
const remoteId = 'abcd-xyz';
|
|
20
|
+
const localId = 'aaa-xyz';
|
|
21
|
+
const result = shouldShowNotification(
|
|
22
|
+
remoteId,
|
|
23
|
+
localId,
|
|
24
|
+
);
|
|
25
|
+
expect(result).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('Returns false if remote presence timestamp is not greater than local', () => {
|
|
29
|
+
const remoteId = 'aaa-xyz';
|
|
30
|
+
const localId = 'abcd-xyz';
|
|
31
|
+
const result = shouldShowNotification(
|
|
32
|
+
remoteId,
|
|
33
|
+
localId,
|
|
34
|
+
);
|
|
35
|
+
expect(result).toBe(false);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('handleJoin', () => {
|
|
40
|
+
it('Adds a new user if not already present', () => {
|
|
41
|
+
const presenceId = 'abcd-xyz';
|
|
42
|
+
const username = 'John';
|
|
43
|
+
const alreadyJoined = { current: new Map() };
|
|
44
|
+
let notifications = [];
|
|
45
|
+
const setNotifications = (updater) => {
|
|
46
|
+
notifications = updater(notifications);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
handleJoin(
|
|
50
|
+
presenceId,
|
|
51
|
+
username,
|
|
52
|
+
alreadyJoined,
|
|
53
|
+
setNotifications,
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
expect(notifications.length).toBe(1);
|
|
57
|
+
expect(notifications[0].user).toBe(username);
|
|
58
|
+
expect(notifications[0].join).toBe(true);
|
|
59
|
+
expect(alreadyJoined.current.has(presenceId)).toBe(
|
|
60
|
+
true,
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('Updates username if user already present', () => {
|
|
65
|
+
const presenceId = 'abcd-xyz';
|
|
66
|
+
const username = 'John';
|
|
67
|
+
const alreadyJoined = {
|
|
68
|
+
current: new Map([[presenceId, 'Doe']]),
|
|
69
|
+
};
|
|
70
|
+
let notifications = [];
|
|
71
|
+
const setNotifications = (updater) => {
|
|
72
|
+
notifications = updater(notifications);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
handleJoin(
|
|
76
|
+
presenceId,
|
|
77
|
+
username,
|
|
78
|
+
alreadyJoined,
|
|
79
|
+
setNotifications,
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
expect(notifications.length).toBe(0);
|
|
83
|
+
expect(alreadyJoined.current.get(presenceId)).toBe(
|
|
84
|
+
username,
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe('handleLeave', () => {
|
|
90
|
+
it('Removes an existing user', () => {
|
|
91
|
+
const presenceId = 'abcd-xyz';
|
|
92
|
+
const username = 'John';
|
|
93
|
+
const alreadyJoined = {
|
|
94
|
+
current: new Map([[presenceId, username]]),
|
|
95
|
+
};
|
|
96
|
+
let notifications = [];
|
|
97
|
+
const setNotifications = (updater) => {
|
|
98
|
+
notifications = updater(notifications);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
handleLeave(
|
|
102
|
+
presenceId,
|
|
103
|
+
alreadyJoined,
|
|
104
|
+
setNotifications,
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
expect(notifications.length).toBe(1);
|
|
108
|
+
expect(notifications[0].user).toBe(username);
|
|
109
|
+
expect(notifications[0].join).toBe(false);
|
|
110
|
+
expect(alreadyJoined.current.has(presenceId)).toBe(
|
|
111
|
+
false,
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('Does nothing if user not present', () => {
|
|
116
|
+
const presenceId = 'abcd-xyz';
|
|
117
|
+
const alreadyJoined = { current: new Map() };
|
|
118
|
+
let notifications = [];
|
|
119
|
+
const setNotifications = (updater) => {
|
|
120
|
+
notifications = updater(notifications);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
handleLeave(
|
|
124
|
+
presenceId,
|
|
125
|
+
alreadyJoined,
|
|
126
|
+
setNotifications,
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
expect(notifications.length).toBe(0);
|
|
130
|
+
expect(alreadyJoined.current.has(presenceId)).toBe(
|
|
131
|
+
false,
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
@@ -1,143 +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<
|
|
81
|
+
useState<PresenceNotification[]>([]);
|
|
25
82
|
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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(
|
|
75
110
|
presenceId,
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
111
|
+
username,
|
|
112
|
+
alreadyJoined,
|
|
113
|
+
setPresenceNotifications,
|
|
114
|
+
);
|
|
115
|
+
} else {
|
|
116
|
+
handleLeave(
|
|
79
117
|
presenceId,
|
|
118
|
+
alreadyJoined,
|
|
119
|
+
setPresenceNotifications,
|
|
80
120
|
);
|
|
81
|
-
setPresenceNotifications((prev) => [
|
|
82
|
-
...prev,
|
|
83
|
-
{ user, join, presenceId },
|
|
84
|
-
]);
|
|
85
|
-
|
|
86
|
-
// Remove it after 5 seconds.
|
|
87
|
-
setTimeout(() => {
|
|
88
|
-
setPresenceNotifications((prev) =>
|
|
89
|
-
prev.filter(
|
|
90
|
-
(notification) =>
|
|
91
|
-
notification.presenceId !==
|
|
92
|
-
presenceId &&
|
|
93
|
-
notification.join !== join,
|
|
94
|
-
),
|
|
95
|
-
);
|
|
96
|
-
}, 5000);
|
|
97
121
|
}
|
|
98
|
-
} else if (!join) {
|
|
99
|
-
setPresenceNotifications((prev) => [
|
|
100
|
-
...prev,
|
|
101
|
-
{ user, join, presenceId },
|
|
102
|
-
]);
|
|
103
122
|
|
|
104
|
-
|
|
105
|
-
// TODO clean up this duplication.
|
|
106
|
-
setTimeout(() => {
|
|
123
|
+
const timeoutId = setTimeout(() => {
|
|
107
124
|
setPresenceNotifications((prev) =>
|
|
108
|
-
prev.
|
|
109
|
-
(notification) =>
|
|
110
|
-
notification.presenceId !== presenceId &&
|
|
111
|
-
notification.join !== join,
|
|
112
|
-
),
|
|
125
|
+
prev.slice(1),
|
|
113
126
|
);
|
|
114
127
|
}, 5000);
|
|
128
|
+
|
|
129
|
+
return () => {
|
|
130
|
+
clearTimeout(timeoutId);
|
|
131
|
+
};
|
|
115
132
|
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
}, [docPresence]);
|
|
133
|
+
};
|
|
119
134
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
// presenceIds,
|
|
127
|
-
// );
|
|
135
|
+
docPresence.on('receive', handleReceive);
|
|
136
|
+
return () => {
|
|
137
|
+
docPresence.off('receive', handleReceive);
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}, [docPresence, localPresence]);
|
|
128
141
|
|
|
129
142
|
return presenceNotifications.length > 0 ? (
|
|
130
143
|
<div className="vz-notification">
|
|
131
|
-
{presenceNotifications.map(
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
144
|
+
{presenceNotifications.map(
|
|
145
|
+
({ user, join, presenceId }) => (
|
|
146
|
+
<div
|
|
147
|
+
key={presenceId}
|
|
148
|
+
className="vz-notification-item"
|
|
149
|
+
>
|
|
135
150
|
{join
|
|
136
151
|
? `${user} has joined the session.`
|
|
137
152
|
: `${user} has left the session.`}
|
|
138
153
|
</div>
|
|
139
|
-
)
|
|
140
|
-
|
|
154
|
+
),
|
|
155
|
+
)}
|
|
141
156
|
</div>
|
|
142
157
|
) : null;
|
|
143
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 }) => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useCallback, useMemo } from 'react';
|
|
2
|
-
import { FileId } from '../../types';
|
|
2
|
+
import type { FileId } from '../../types';
|
|
3
|
+
import type { TabState } from '../vzReducer';
|
|
3
4
|
|
|
4
5
|
// Supports adding the file's containing folder to the tab name
|
|
5
6
|
const fileNameSplit = (fileName: string) => {
|
|
@@ -12,14 +13,18 @@ const fileNameSplit = (fileName: string) => {
|
|
|
12
13
|
|
|
13
14
|
export const Tab = ({
|
|
14
15
|
fileId,
|
|
16
|
+
isTransient,
|
|
15
17
|
isActive,
|
|
16
18
|
setActiveFileId,
|
|
19
|
+
openTab,
|
|
17
20
|
closeTabs,
|
|
18
21
|
fileName,
|
|
19
22
|
}: {
|
|
20
23
|
fileId: FileId;
|
|
24
|
+
isTransient: boolean;
|
|
21
25
|
isActive: boolean;
|
|
22
26
|
setActiveFileId: (fileId: FileId) => void;
|
|
27
|
+
openTab: (tabState: TabState) => void;
|
|
23
28
|
closeTabs: (fileIds: FileId[]) => void;
|
|
24
29
|
fileName: string;
|
|
25
30
|
}) => {
|
|
@@ -38,16 +43,25 @@ export const Tab = ({
|
|
|
38
43
|
[fileName],
|
|
39
44
|
);
|
|
40
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
|
+
|
|
41
55
|
return (
|
|
42
56
|
<div
|
|
43
|
-
className={
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
57
|
+
className={`tab user-select-none ${
|
|
58
|
+
isActive ? 'active' : ''
|
|
59
|
+
} ${isTransient ? 'transient' : ''}`}
|
|
60
|
+
onClick={handleClick}
|
|
61
|
+
onDoubleClick={handleDoubleClick}
|
|
47
62
|
>
|
|
48
63
|
{tabName}
|
|
49
64
|
<div
|
|
50
|
-
// className={isActive ? 'bx bx-x tab-close' : ''}
|
|
51
65
|
className={'bx bx-x tab-close'}
|
|
52
66
|
onClick={handleCloseClick}
|
|
53
67
|
></div>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useCallback, useEffect } from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import type { TabState } from '../vzReducer';
|
|
3
|
+
import type { FileId, Files } from '../../types';
|
|
3
4
|
import { Tab } from './Tab';
|
|
4
5
|
import './style.scss';
|
|
5
6
|
|
|
@@ -9,13 +10,17 @@ export const TabList = ({
|
|
|
9
10
|
tabList,
|
|
10
11
|
activeFileId,
|
|
11
12
|
setActiveFileId,
|
|
13
|
+
openTab,
|
|
12
14
|
closeTabs,
|
|
15
|
+
createFile,
|
|
13
16
|
}: {
|
|
14
17
|
files: Files;
|
|
15
|
-
tabList:
|
|
18
|
+
tabList: Array<TabState>;
|
|
16
19
|
activeFileId: FileId;
|
|
17
20
|
setActiveFileId: (fileId: FileId) => void;
|
|
21
|
+
openTab: (tabState: TabState) => void;
|
|
18
22
|
closeTabs: (fileIds: FileId[]) => void;
|
|
23
|
+
createFile: () => void;
|
|
19
24
|
}) => {
|
|
20
25
|
// Close the active tab on alt+w
|
|
21
26
|
const handleKeyPress = useCallback(
|
|
@@ -24,9 +29,12 @@ export const TabList = ({
|
|
|
24
29
|
if (event.key == 'w') {
|
|
25
30
|
closeTabs([activeFileId]);
|
|
26
31
|
}
|
|
32
|
+
if (event.key == 'n') {
|
|
33
|
+
createFile();
|
|
34
|
+
}
|
|
27
35
|
}
|
|
28
36
|
},
|
|
29
|
-
[closeTabs, activeFileId],
|
|
37
|
+
[createFile, closeTabs, activeFileId],
|
|
30
38
|
);
|
|
31
39
|
|
|
32
40
|
// Add the global keydown event listener
|
|
@@ -43,14 +51,16 @@ export const TabList = ({
|
|
|
43
51
|
return (
|
|
44
52
|
<div className="vz-tab-list">
|
|
45
53
|
{files &&
|
|
46
|
-
tabList.map((
|
|
54
|
+
tabList.map((tabState: TabState) => (
|
|
47
55
|
<Tab
|
|
48
|
-
key={fileId}
|
|
49
|
-
fileId={fileId}
|
|
50
|
-
|
|
56
|
+
key={tabState.fileId}
|
|
57
|
+
fileId={tabState.fileId}
|
|
58
|
+
isTransient={tabState.isTransient}
|
|
59
|
+
isActive={tabState.fileId === activeFileId}
|
|
51
60
|
setActiveFileId={setActiveFileId}
|
|
61
|
+
openTab={openTab}
|
|
52
62
|
closeTabs={closeTabs}
|
|
53
|
-
fileName={files[fileId].name}
|
|
63
|
+
fileName={files[tabState.fileId].name}
|
|
54
64
|
/>
|
|
55
65
|
))}
|
|
56
66
|
</div>
|
|
@@ -22,6 +22,7 @@
|
|
|
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;
|
|
27
28
|
margin-bottom: 2px;
|
|
@@ -45,4 +46,8 @@
|
|
|
45
46
|
.tab.active {
|
|
46
47
|
background-color: #202e46;
|
|
47
48
|
}
|
|
49
|
+
|
|
50
|
+
.tab.transient {
|
|
51
|
+
font-style: italic;
|
|
52
|
+
}
|
|
48
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 let username = 'Anonymous';
|
|
13
|
-
|
|
14
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 VZSettings = ({
|
|
|
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 VZSettings = ({
|
|
|
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
|
|