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
|
@@ -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
|
+
};
|
|
@@ -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
|
-
|
|
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
|
-
(
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
+
});
|