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.
Files changed (55) hide show
  1. package/dist/assets/index-5f9f376e.css +1 -0
  2. package/dist/assets/index-70b6d947.js +111 -0
  3. package/dist/index.html +2 -6
  4. package/package.json +7 -7
  5. package/src/client/App.tsx +110 -247
  6. package/src/client/CodeEditor/getOrCreateEditor.ts +6 -1
  7. package/src/client/CodeEditor/index.tsx +94 -16
  8. package/src/client/CodeEditor/{json1PresenceBroadcast.js → json1PresenceBroadcast.ts} +9 -3
  9. package/src/client/CodeEditor/{json1PresenceDisplay.js → json1PresenceDisplay.ts} +9 -48
  10. package/src/client/CodeEditor/style.scss +5 -1
  11. package/src/client/CodeErrorOverlay/index.tsx +27 -0
  12. package/src/client/CodeErrorOverlay/style.scss +30 -0
  13. package/src/client/PresenceNotifications/PresenceNotifications.test.ts +134 -0
  14. package/src/client/PresenceNotifications/index.tsx +119 -106
  15. package/src/client/SplitPaneResizeContext.tsx +12 -7
  16. package/src/client/TabList/Tab.tsx +70 -0
  17. package/src/client/TabList/index.tsx +28 -69
  18. package/src/client/TabList/style.scss +26 -9
  19. package/src/client/{Settings.tsx → VZSettings.tsx} +34 -21
  20. package/src/client/{Sidebar/ConfirmationBox.tsx → VZSidebar/DeleteConfirmationModal.tsx} +17 -14
  21. package/src/client/{Sidebar → VZSidebar}/DirectoryListing.tsx +27 -26
  22. package/src/client/{Sidebar → VZSidebar}/FileListing.tsx +19 -19
  23. package/src/client/VZSidebar/Item.tsx +179 -0
  24. package/src/client/{Sidebar → VZSidebar}/Listing.tsx +20 -14
  25. package/src/client/{Sidebar → VZSidebar}/index.tsx +48 -30
  26. package/src/client/{Sidebar → VZSidebar}/styles.scss +17 -12
  27. package/src/client/bootstrap.ts +3 -0
  28. package/src/client/style.scss +8 -2
  29. package/src/client/useActions.ts +93 -0
  30. package/src/client/useEditorCache.ts +2 -1
  31. package/src/client/useFileCRUD.ts +103 -0
  32. package/src/client/useOpenDirectories.ts +25 -7
  33. package/src/client/useSubmitOperation.ts +25 -0
  34. package/src/client/usernameLocalStorage.ts +43 -0
  35. package/src/client/vzReducer/closeTabReducer.test.ts +177 -0
  36. package/src/client/vzReducer/closeTabsReducer.ts +68 -0
  37. package/src/client/vzReducer/createInitialState.ts +19 -0
  38. package/src/client/vzReducer/editorNoLongerWantsFocusReducer.ts +9 -0
  39. package/src/client/vzReducer/index.ts +107 -0
  40. package/src/client/vzReducer/openTabReducer.test.ts +110 -0
  41. package/src/client/vzReducer/openTabReducer.ts +57 -0
  42. package/src/client/vzReducer/setActiveFileIdReducer.ts +9 -0
  43. package/src/client/vzReducer/setIsSettingsOpenReducer.ts +9 -0
  44. package/src/client/vzReducer/setThemeReducer.ts +9 -0
  45. package/src/client/vzReducer/setUsernameReducer.ts +9 -0
  46. package/src/index.ts +40 -0
  47. package/src/server/index.js +13 -2
  48. package/src/types.ts +12 -5
  49. package/dist/assets/index-8628ebab.js +0 -111
  50. package/dist/assets/index-cf19de3b.css +0 -1
  51. package/src/client/PrettierErrorOverlay/index.tsx +0 -9
  52. package/src/client/PrettierErrorOverlay/style.scss +0 -9
  53. package/src/client/Sidebar/Item.tsx +0 -130
  54. package/src/client/reducer.ts +0 -119
  55. /package/src/client/{Sidebar → VZSidebar}/DirectoryArrowSVG.jsx +0 -0
@@ -0,0 +1,93 @@
1
+ import { useCallback } from 'react';
2
+ import { ThemeLabel } from './themes';
3
+ import { FileId, Username } from '../types';
4
+ import { TabState, VZAction } from './vzReducer';
5
+
6
+ // This is a custom hook that returns a set of functions
7
+ // that can be used to dispatch actions to the reducer.
8
+ export const useActions = (
9
+ dispatch: (action: VZAction) => void,
10
+ ) => {
11
+ const setActiveFileId = useCallback(
12
+ (activeFileId: FileId) => {
13
+ dispatch({
14
+ type: 'set_active_file_id',
15
+ activeFileId,
16
+ });
17
+ },
18
+ [dispatch],
19
+ );
20
+
21
+ const openTab = useCallback(
22
+ (tabState: TabState): void => {
23
+ dispatch({
24
+ type: 'open_tab',
25
+ fileId: tabState.fileId,
26
+ isTransient: tabState.isTransient,
27
+ });
28
+ },
29
+ [dispatch],
30
+ );
31
+
32
+ const closeTabs = useCallback(
33
+ (fileIdsToClose: Array<FileId>) => {
34
+ dispatch({
35
+ type: 'close_tabs',
36
+ fileIdsToClose,
37
+ });
38
+ },
39
+ [dispatch],
40
+ );
41
+
42
+ const setTheme = useCallback(
43
+ (themeLabel: ThemeLabel) => {
44
+ dispatch({
45
+ type: 'set_theme',
46
+ themeLabel: themeLabel,
47
+ });
48
+ },
49
+ [dispatch],
50
+ );
51
+
52
+ // True to show the settings modal.
53
+ const setIsSettingsOpen = useCallback(
54
+ (value: boolean) => {
55
+ dispatch({
56
+ type: 'set_is_settings_open',
57
+ value: value,
58
+ });
59
+ },
60
+ [dispatch],
61
+ );
62
+
63
+ const closeSettings = useCallback(() => {
64
+ setIsSettingsOpen(false);
65
+ }, [setIsSettingsOpen]);
66
+
67
+ const editorNoLongerWantsFocus = useCallback(() => {
68
+ dispatch({
69
+ type: 'editor_no_longer_wants_focus',
70
+ });
71
+ }, [dispatch]);
72
+
73
+ const setUsername = useCallback(
74
+ (username: Username) => {
75
+ dispatch({
76
+ type: 'set_username',
77
+ username,
78
+ });
79
+ },
80
+ [dispatch],
81
+ );
82
+
83
+ return {
84
+ setActiveFileId,
85
+ openTab,
86
+ closeTabs,
87
+ setTheme,
88
+ setIsSettingsOpen,
89
+ closeSettings,
90
+ editorNoLongerWantsFocus,
91
+ setUsername,
92
+ };
93
+ };
@@ -1,8 +1,9 @@
1
1
  import { useRef } from 'react';
2
2
  import { FileId } from '../types';
3
+ import { EditorView } from 'codemirror';
3
4
 
4
5
  export type EditorCacheValue = {
5
- editor: any;
6
+ editor: EditorView;
6
7
  themeCompartment: any;
7
8
  };
8
9
 
@@ -0,0 +1,103 @@
1
+ import { useCallback } from 'react';
2
+ import {
3
+ FileId,
4
+ FileTreePath,
5
+ VZCodeContent,
6
+ } from '../types';
7
+ import { randomId } from '../randomId';
8
+
9
+ // CRUD operations for files and directories
10
+ // CRUD = Create, Read, Update, Delete
11
+ export const useFileCRUD = ({
12
+ submitOperation,
13
+ closeTabs,
14
+ openTab,
15
+ }: {
16
+ submitOperation: (
17
+ operation: (document: VZCodeContent) => VZCodeContent,
18
+ ) => void;
19
+ closeTabs: (idsToDelete: Array<FileId>) => void;
20
+ openTab: ({
21
+ fileId,
22
+ isTransient,
23
+ }: {
24
+ fileId: FileId;
25
+ isTransient: boolean;
26
+ }) => void;
27
+ }) => {
28
+ // Create a new file
29
+ const createFile = useCallback(() => {
30
+ // TODO use Bootstrap modals not native prompts
31
+ // See https://github.com/vizhub-core/vzcode/issues/252
32
+ const name = prompt('Enter new file name');
33
+ if (name) {
34
+ const fileId: FileId = randomId();
35
+ submitOperation((document: VZCodeContent) => ({
36
+ ...document,
37
+ files: {
38
+ ...document.files,
39
+ [fileId]: { name, text: '' },
40
+ },
41
+ }));
42
+ // When a new file is created, open it in a new tab
43
+ // and focus the editor on it.
44
+ openTab({ fileId, isTransient: false });
45
+ }
46
+ }, [submitOperation]);
47
+
48
+ // Called when a file in the sidebar is renamed.
49
+ const renameFile = useCallback(
50
+ (fileId: FileId, newName: string) => {
51
+ submitOperation((document: VZCodeContent) => ({
52
+ ...document,
53
+ files: {
54
+ ...document.files,
55
+ [fileId]: {
56
+ ...document.files[fileId],
57
+ name: newName,
58
+ },
59
+ },
60
+ }));
61
+ },
62
+ [submitOperation],
63
+ );
64
+
65
+ // Deletes a file
66
+ const deleteFile = useCallback(
67
+ (fileId: FileId) => {
68
+ closeTabs([fileId]);
69
+ submitOperation((document) => {
70
+ const updatedFiles = { ...document.files };
71
+ delete updatedFiles[fileId];
72
+ return { ...document, files: updatedFiles };
73
+ });
74
+ },
75
+ [submitOperation, closeTabs],
76
+ );
77
+
78
+ // Deletes a directory and all files within it
79
+ const deleteDirectory = useCallback(
80
+ (path: FileTreePath) => {
81
+ const tabsToClose: Array<FileId> = [];
82
+ submitOperation((document: VZCodeContent) => {
83
+ const updatedFiles = { ...document.files };
84
+ for (const key in updatedFiles) {
85
+ if (updatedFiles[key].name.includes(path)) {
86
+ tabsToClose.push(key);
87
+ delete updatedFiles[key];
88
+ }
89
+ }
90
+ return { ...document, files: updatedFiles };
91
+ });
92
+ closeTabs(tabsToClose);
93
+ },
94
+ [submitOperation, closeTabs],
95
+ );
96
+
97
+ return {
98
+ createFile,
99
+ renameFile,
100
+ deleteFile,
101
+ deleteDirectory,
102
+ };
103
+ };
@@ -17,6 +17,7 @@ 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
+ // TODO move this into reducer
20
21
  export const useOpenDirectories = () => {
21
22
  // The set of open directories by path/
22
23
  const [openDirectories, setOpenDirectories] = useState<
@@ -30,14 +31,31 @@ export const useOpenDirectories = () => {
30
31
  [openDirectories],
31
32
  );
32
33
 
33
- // Toggle whether a directory is open.
34
+ /**
35
+ * Toggles the state of a directory (open/close).
36
+ * If the directory is currently open, it will close it.
37
+ * If it's closed, it will open it.
38
+ */
34
39
  const toggleDirectory = useCallback(
35
- (path) => {
36
- const newOpenDirectories = new Set(openDirectories);
37
- newOpenDirectories[
38
- isDirectoryOpen(path) ? 'delete' : 'add'
39
- ](path);
40
- setOpenDirectories(newOpenDirectories);
40
+ (directoryPath: string) => {
41
+ // Create a copy of the currently open directories
42
+ const updatedOpenDirectories = new Set(
43
+ openDirectories,
44
+ );
45
+
46
+ // Check if the directory is currently open
47
+ const directoryIsOpen =
48
+ isDirectoryOpen(directoryPath);
49
+
50
+ // Toggle the directory's state
51
+ if (directoryIsOpen) {
52
+ updatedOpenDirectories.delete(directoryPath);
53
+ } else {
54
+ updatedOpenDirectories.add(directoryPath);
55
+ }
56
+
57
+ // Update the state with the modified set of open directories
58
+ setOpenDirectories(updatedOpenDirectories);
41
59
  },
42
60
  [openDirectories, isDirectoryOpen],
43
61
  );
@@ -0,0 +1,25 @@
1
+ import { useCallback } from 'react';
2
+ import { ShareDBDoc, VZCodeContent } from '../types';
3
+ import { diff } from './diff';
4
+
5
+ // A helper function to submit diff-based operations to ShareDB
6
+ export const useSubmitOperation = (
7
+ shareDBDoc: ShareDBDoc<VZCodeContent>,
8
+ ): ((
9
+ next: (content: VZCodeContent) => VZCodeContent,
10
+ ) => void) => {
11
+ const submitOperation: (
12
+ next: (content: VZCodeContent) => VZCodeContent,
13
+ ) => void = useCallback(
14
+ (next) => {
15
+ const content: VZCodeContent = shareDBDoc.data;
16
+ const op = diff(content, next(content));
17
+ if (op && shareDBDoc) {
18
+ shareDBDoc.submitOp(op);
19
+ }
20
+ },
21
+ [shareDBDoc],
22
+ );
23
+
24
+ return submitOperation;
25
+ };
@@ -0,0 +1,43 @@
1
+ // This module deals with storing and accessing the
2
+
3
+ import { useEffect, useRef } from 'react';
4
+ import { Username } from '../types';
5
+
6
+ const localStoragePropertyName = 'vzCodeUsername';
7
+
8
+ const initialUsernameDefault = 'Anonymous';
9
+
10
+ // Gets the username from local storage.
11
+ export const useInitialUsername = (): Username => {
12
+ let initialUsername: Username = initialUsernameDefault;
13
+
14
+ // If we're in the browser,
15
+ if (typeof window !== 'undefined') {
16
+ //check localStorage for a previously stored width.
17
+ const initialUsernameFromLocalStorage: string | null =
18
+ window.localStorage.getItem(localStoragePropertyName);
19
+
20
+ // If there is a previously stored width,
21
+ if (initialUsernameFromLocalStorage !== null) {
22
+ // use it as the initial width.
23
+ initialUsername = initialUsernameFromLocalStorage;
24
+ }
25
+ } else {
26
+ // If we're not in the browser, use the default initial width.
27
+ }
28
+ return initialUsername;
29
+ };
30
+
31
+ // Stores the username to local storage.
32
+ export const usePersistUsername = (username: Username) => {
33
+ const previousUsername = useRef<Username>(username);
34
+ useEffect(() => {
35
+ if (previousUsername.current !== username) {
36
+ previousUsername.current = username;
37
+ window.localStorage.setItem(
38
+ 'vzCodeUsername',
39
+ username,
40
+ );
41
+ }
42
+ }, [username]);
43
+ };
@@ -0,0 +1,177 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { closeTabsReducer } from './closeTabsReducer';
3
+ import { VZAction, VZState, createInitialState } from '.';
4
+ import { defaultTheme } from '../themes';
5
+
6
+ const initialState: VZState = createInitialState({
7
+ defaultTheme,
8
+ });
9
+
10
+ describe('closeTabsReducer', () => {
11
+ it('Closes an existing tab', () => {
12
+ const action: VZAction = {
13
+ type: 'close_tabs',
14
+ fileIdsToClose: ['file1'],
15
+ };
16
+
17
+ const stateWithTabs = {
18
+ ...initialState,
19
+ tabList: [{ fileId: 'file1', isTransient: false }],
20
+ activeFileId: 'file1',
21
+ };
22
+
23
+ const newState = closeTabsReducer(
24
+ stateWithTabs,
25
+ action,
26
+ );
27
+
28
+ expect(newState.tabList.length).toBe(0);
29
+ expect(newState.activeFileId).toBeNull();
30
+ });
31
+
32
+ it('Keeps other tabs open', () => {
33
+ const action: VZAction = {
34
+ type: 'close_tabs',
35
+ fileIdsToClose: ['file2'],
36
+ };
37
+ const stateWithTabs = {
38
+ ...initialState,
39
+ tabList: [
40
+ { fileId: 'file1', isTransient: false },
41
+ { fileId: 'file2', isTransient: false },
42
+ ],
43
+ activeFileId: 'file1',
44
+ };
45
+
46
+ const newState = closeTabsReducer(
47
+ stateWithTabs,
48
+ action,
49
+ );
50
+
51
+ expect(newState.tabList.length).toBe(1);
52
+ expect(newState.tabList[0].fileId).toBe('file1');
53
+ expect(newState.activeFileId).toBe('file1');
54
+ });
55
+
56
+ it('Activates previous tab after closing an active tab', () => {
57
+ const action: VZAction = {
58
+ type: 'close_tabs',
59
+ fileIdsToClose: ['file2'],
60
+ };
61
+ const stateWithTabs = {
62
+ ...initialState,
63
+ tabList: [
64
+ { fileId: 'file1', isTransient: false },
65
+ { fileId: 'file2', isTransient: false },
66
+ { fileId: 'file3', isTransient: false },
67
+ ],
68
+ activeFileId: 'file2',
69
+ };
70
+
71
+ const newState = closeTabsReducer(
72
+ stateWithTabs,
73
+ action,
74
+ );
75
+
76
+ expect(newState.tabList.length).toBe(2);
77
+ expect(newState.activeFileId).toBe('file1'); // should switch to the previous tab
78
+ });
79
+
80
+ it('Does not modify the state if closing non-existing tabs', () => {
81
+ const action: VZAction = {
82
+ type: 'close_tabs',
83
+ fileIdsToClose: ['file4'],
84
+ };
85
+ const stateWithTabs = {
86
+ ...initialState,
87
+ tabList: [
88
+ { fileId: 'file1', isTransient: false },
89
+ { fileId: 'file2', isTransient: false },
90
+ { fileId: 'file3', isTransient: false },
91
+ ],
92
+ activeFileId: 'file2',
93
+ };
94
+
95
+ const newState = closeTabsReducer(
96
+ stateWithTabs,
97
+ action,
98
+ );
99
+
100
+ expect(newState).toEqual(stateWithTabs);
101
+ });
102
+
103
+ it('Closes a tab that is not currently active', () => {
104
+ const stateWithMultipleTabs = {
105
+ ...initialState,
106
+ tabList: [
107
+ { fileId: 'file1', isTransient: false },
108
+ { fileId: 'file2', isTransient: false },
109
+ { fileId: 'file3', isTransient: false },
110
+ ],
111
+ activeFileId: 'file3',
112
+ };
113
+
114
+ const action: VZAction = {
115
+ type: 'close_tabs',
116
+ fileIdsToClose: ['file2'],
117
+ };
118
+
119
+ const newState = closeTabsReducer(
120
+ stateWithMultipleTabs,
121
+ action,
122
+ );
123
+
124
+ expect(newState.tabList.length).toBe(2);
125
+ expect(newState.activeFileId).toBe('file3'); // remains unchanged
126
+ });
127
+
128
+ it('Closes the first tab out of many, when it is active', () => {
129
+ const stateWithMultipleTabs = {
130
+ ...initialState,
131
+ tabList: [
132
+ { fileId: 'file1', isTransient: false },
133
+ { fileId: 'file2', isTransient: false },
134
+ { fileId: 'file3', isTransient: false },
135
+ ],
136
+ activeFileId: 'file1',
137
+ };
138
+
139
+ const action: VZAction = {
140
+ type: 'close_tabs',
141
+ fileIdsToClose: ['file1'],
142
+ };
143
+
144
+ const newState = closeTabsReducer(
145
+ stateWithMultipleTabs,
146
+ action,
147
+ );
148
+
149
+ expect(newState.tabList.length).toBe(2);
150
+ expect(newState.activeFileId).toBe('file2'); // switches to the next available tab
151
+ });
152
+
153
+ it('Closes multiple tabs at once', () => {
154
+ const stateWithMultipleTabs = {
155
+ ...initialState,
156
+ tabList: [
157
+ { fileId: 'file1', isTransient: false },
158
+ { fileId: 'file2', isTransient: false },
159
+ { fileId: 'file3', isTransient: false },
160
+ ],
161
+ activeFileId: 'file2',
162
+ };
163
+
164
+ const action: VZAction = {
165
+ type: 'close_tabs',
166
+ fileIdsToClose: ['file1', 'file2'],
167
+ };
168
+
169
+ const newState = closeTabsReducer(
170
+ stateWithMultipleTabs,
171
+ action,
172
+ );
173
+
174
+ expect(newState.tabList.length).toBe(1);
175
+ expect(newState.activeFileId).toBe('file3'); // switches to the next available tab
176
+ });
177
+ });
@@ -0,0 +1,68 @@
1
+ import { TabState, VZAction, VZState } from '.';
2
+ import { FileId } from '../../types';
3
+
4
+ export const closeTabsReducer = (
5
+ state: VZState,
6
+ action: VZAction,
7
+ ): VZState => {
8
+ if (action.type !== 'close_tabs') {
9
+ return state;
10
+ }
11
+
12
+ const newTabList: Array<TabState> = state.tabList.filter(
13
+ (tabState) =>
14
+ !action.fileIdsToClose.includes(tabState.fileId),
15
+ );
16
+
17
+ // If the active tab wasn't closed, keep it active.
18
+ if (
19
+ !action.fileIdsToClose.includes(state.activeFileId!)
20
+ ) {
21
+ return {
22
+ ...state,
23
+ tabList: newTabList,
24
+ };
25
+ }
26
+
27
+ const originalIndex = state.tabList.findIndex(
28
+ (tabState) => tabState.fileId === state.activeFileId,
29
+ );
30
+
31
+ let newActiveFileId: FileId | null = null;
32
+
33
+ // Try getting the previous available tab first.
34
+ for (let i = originalIndex - 1; i >= 0; i--) {
35
+ if (
36
+ !action.fileIdsToClose.includes(
37
+ state.tabList[i].fileId,
38
+ )
39
+ ) {
40
+ newActiveFileId = state.tabList[i].fileId;
41
+ break;
42
+ }
43
+ }
44
+
45
+ // If no previous tab available, get the next available one.
46
+ if (!newActiveFileId) {
47
+ for (
48
+ let i = originalIndex + 1;
49
+ i < state.tabList.length;
50
+ i++
51
+ ) {
52
+ if (
53
+ !action.fileIdsToClose.includes(
54
+ state.tabList[i].fileId,
55
+ )
56
+ ) {
57
+ newActiveFileId = state.tabList[i].fileId;
58
+ break;
59
+ }
60
+ }
61
+ }
62
+
63
+ return {
64
+ ...state,
65
+ tabList: newTabList,
66
+ activeFileId: newActiveFileId,
67
+ };
68
+ };
@@ -0,0 +1,19 @@
1
+ import { VZState } from '.';
2
+ import { ThemeLabel } from '../themes';
3
+
4
+ export const createInitialState = ({
5
+ defaultTheme,
6
+ initialUsername,
7
+ }: {
8
+ defaultTheme: ThemeLabel;
9
+ initialUsername?: string;
10
+ }): VZState => ({
11
+ tabList: [],
12
+ activeFileId: null,
13
+ theme: defaultTheme,
14
+ isSettingsOpen: false,
15
+ editorWantsFocus: false,
16
+
17
+ // TODO unify this with App.tsx
18
+ username: initialUsername || 'Anonymous',
19
+ });
@@ -0,0 +1,9 @@
1
+ import { VZAction, VZState } from '.';
2
+
3
+ export const editorNoLongerWantsFocusReducer = (
4
+ state: VZState,
5
+ action: VZAction,
6
+ ): VZState =>
7
+ action.type === 'editor_no_longer_wants_focus'
8
+ ? { ...state, editorWantsFocus: false }
9
+ : state;