vzcode 0.25.0 → 0.26.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 (32) hide show
  1. package/dist/assets/index-284b7339.js +111 -0
  2. package/dist/assets/index-e160f758.css +1 -0
  3. package/dist/index.html +2 -6
  4. package/package.json +5 -5
  5. package/src/client/App.tsx +45 -225
  6. package/src/client/CodeEditor/index.tsx +36 -6
  7. package/src/client/CodeEditor/{json1PresenceBroadcast.js → json1PresenceBroadcast.ts} +5 -3
  8. package/src/client/CodeEditor/{json1PresenceDisplay.js → json1PresenceDisplay.ts} +9 -3
  9. package/src/client/PresenceNotifications/index.tsx +0 -2
  10. package/src/client/TabList/Tab.tsx +56 -0
  11. package/src/client/TabList/index.tsx +11 -62
  12. package/src/client/TabList/style.scss +21 -9
  13. package/src/client/{Settings.tsx → VZSettings.tsx} +1 -1
  14. package/src/client/{Sidebar → VZSidebar}/FileListing.tsx +3 -0
  15. package/src/client/{Sidebar → VZSidebar}/Item.tsx +7 -4
  16. package/src/client/{Sidebar → VZSidebar}/Listing.tsx +3 -0
  17. package/src/client/{Sidebar → VZSidebar}/index.tsx +5 -6
  18. package/src/client/{Sidebar → VZSidebar}/styles.scss +17 -12
  19. package/src/client/bootstrap.ts +3 -0
  20. package/src/client/style.scss +5 -2
  21. package/src/client/useActions.ts +74 -0
  22. package/src/client/useFileCRUD.ts +101 -0
  23. package/src/client/useOpenDirectories.ts +25 -7
  24. package/src/client/useSubmitOperation.ts +25 -0
  25. package/src/client/{reducer.ts → vzReducer.ts} +42 -43
  26. package/src/index.ts +34 -0
  27. package/src/server/index.js +13 -2
  28. package/dist/assets/index-8628ebab.js +0 -111
  29. package/dist/assets/index-cf19de3b.css +0 -1
  30. /package/src/client/{Sidebar → VZSidebar}/ConfirmationBox.tsx +0 -0
  31. /package/src/client/{Sidebar → VZSidebar}/DirectoryArrowSVG.jsx +0 -0
  32. /package/src/client/{Sidebar → VZSidebar}/DirectoryListing.tsx +0 -0
@@ -1,20 +1,20 @@
1
1
  .vz-tab-list {
2
2
  display: flex;
3
- background-color: rgb(37, 37, 38);
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: rgb(45, 45, 45);
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: 10px;
15
- padding-right: 10px;
16
- border-top-left-radius: 10px;
17
- border-top-right-radius: 10px;
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;
@@ -24,13 +24,25 @@
24
24
 
25
25
  .tab-close {
26
26
  margin-left: 5px;
27
+ margin-bottom: 2px;
27
28
  }
28
29
  .tab-close:hover {
29
- border-radius: 30%;
30
- background-color: rgb(37, 37, 38);
30
+ border-radius: 50%;
31
+ background-color: rgba(255, 255, 255, 0.2);
32
+ }
33
+
34
+ // No left margin for first child
35
+ &:first-child {
36
+ margin-left: 0;
37
+ }
38
+
39
+ // on hover
40
+ &:hover {
41
+ background-color: #47546b;
31
42
  }
32
43
  }
44
+
33
45
  .tab.active {
34
- background-color: rgb(41, 44, 52);
46
+ background-color: #202e46;
35
47
  }
36
48
  }
@@ -11,7 +11,7 @@ import { ThemeLabel, themes } from './themes';
11
11
 
12
12
  export let username = 'Anonymous';
13
13
 
14
- export const Settings = ({
14
+ export const VZSettings = ({
15
15
  show,
16
16
  onClose,
17
17
  setTheme,
@@ -8,6 +8,7 @@ export const FileListing = ({
8
8
  handleRenameFileClick,
9
9
  handleDeleteFileClick,
10
10
  handleFileClick,
11
+ isActive,
11
12
  }: {
12
13
  name: string;
13
14
  fileId: FileId;
@@ -20,6 +21,7 @@ export const FileListing = ({
20
21
  event: React.MouseEvent,
21
22
  ) => void;
22
23
  handleFileClick: (fileId: FileId) => void;
24
+ isActive: boolean;
23
25
  }) => {
24
26
  const handleClick = useCallback(() => {
25
27
  handleFileClick(fileId);
@@ -46,6 +48,7 @@ export const FileListing = ({
46
48
  handleDeleteClick={handleDeleteClick}
47
49
  handleRenameClick={handleRenameClick}
48
50
  isDir={false}
51
+ isActive={isActive}
49
52
  >
50
53
  {name}
51
54
  </Item>
@@ -8,6 +8,7 @@ export const Item = ({
8
8
  handleRenameClick,
9
9
  handleDeleteClick,
10
10
  isDir,
11
+ isActive = false,
11
12
  }) => {
12
13
  // Tracks whether the mouse is hovering over the file or directory
13
14
  const [isHovered, setIsHovered] = useState(false);
@@ -82,7 +83,9 @@ export const Item = ({
82
83
 
83
84
  return (
84
85
  <div
85
- className="full-box file-or-directory"
86
+ className={`full-box file-or-directory ${
87
+ isActive ? 'active-file' : ''
88
+ }`}
86
89
  onClick={isRenaming ? null : handleClick}
87
90
  onMouseEnter={handleMouseEnter}
88
91
  onMouseLeave={handleMouseLeave}
@@ -105,7 +108,7 @@ export const Item = ({
105
108
  <div className="utils">
106
109
  <i
107
110
  className="bx bxs-edit utilities"
108
- style={{ color: '#abdafb' }}
111
+ style={{ color: '#9edcff' }}
109
112
  onClick={(event: React.MouseEvent) => {
110
113
  event.stopPropagation();
111
114
  setIsRenaming(!isRenaming);
@@ -113,8 +116,8 @@ export const Item = ({
113
116
  }}
114
117
  ></i>
115
118
  <i
116
- className="bx bx-trash"
117
- style={{ color: '#eb336c' }}
119
+ className="bx bx-trash utilities"
120
+ style={{ color: '#ff006b' }}
118
121
  onClick={handleModalOpen}
119
122
  ></i>
120
123
  </div>
@@ -15,6 +15,7 @@ export const Listing = ({
15
15
  handleFileClick,
16
16
  isDirectoryOpen,
17
17
  toggleDirectory,
18
+ activeFileId,
18
19
  }: {
19
20
  entity: FileTree | FileTreeFile;
20
21
  handleRenameFileClick: (
@@ -28,6 +29,7 @@ export const Listing = ({
28
29
  handleFileClick: (fileId: FileId) => void;
29
30
  isDirectoryOpen: (path: string) => boolean;
30
31
  toggleDirectory: (path: string) => void;
32
+ activeFileId: FileId;
31
33
  }) => {
32
34
  const { name, file, fileId } = entity as FileTreeFile;
33
35
  const { path, children } = entity as FileTree;
@@ -38,6 +40,7 @@ export const Listing = ({
38
40
  handleRenameFileClick={handleRenameFileClick}
39
41
  handleDeleteFileClick={handleDeleteFileClick}
40
42
  handleFileClick={handleFileClick}
43
+ isActive={fileId === activeFileId}
41
44
  />
42
45
  ) : (
43
46
  <DirectoryListing
@@ -12,7 +12,7 @@ import { SplitPaneResizeContext } from '../SplitPaneResizeContext';
12
12
  import { Listing } from './Listing';
13
13
  import './styles.scss';
14
14
 
15
- export const Sidebar = ({
15
+ export const VZSidebar = ({
16
16
  createFile,
17
17
  files,
18
18
  handleRenameFileClick,
@@ -21,6 +21,7 @@ export const Sidebar = ({
21
21
  setIsSettingsOpen,
22
22
  isDirectoryOpen,
23
23
  toggleDirectory,
24
+ activeFileId,
24
25
  }: {
25
26
  files: Files;
26
27
  createFile?: () => void;
@@ -36,6 +37,7 @@ export const Sidebar = ({
36
37
  setIsSettingsOpen?: (isSettingsOpen: boolean) => void;
37
38
  isDirectoryOpen?: (path: string) => boolean;
38
39
  toggleDirectory?: (path: string) => void;
40
+ activeFileId?: FileId;
39
41
  }) => {
40
42
  const fileTree = useMemo(
41
43
  () => (files ? sortFileTree(getFileTree(files)) : null),
@@ -57,11 +59,7 @@ export const Sidebar = ({
57
59
  >
58
60
  <div className="files">
59
61
  <div className="full-box">
60
- <div>
61
- <a className="link-name" href="#">
62
- Files
63
- </a>
64
- </div>
62
+ <div className="sidebar-section-hint">Files</div>
65
63
  <div>
66
64
  <i
67
65
  className="bx bxs-file-plus new-btn"
@@ -88,6 +86,7 @@ export const Sidebar = ({
88
86
  handleFileClick={handleFileClick}
89
87
  isDirectoryOpen={isDirectoryOpen}
90
88
  toggleDirectory={toggleDirectory}
89
+ activeFileId={activeFileId}
91
90
  />
92
91
  );
93
92
  })
@@ -1,12 +1,13 @@
1
1
  .vz-sidebar {
2
2
  height: 100%;
3
- background: rgb(54, 65, 89);
3
+ color: #d6d9de;
4
+ background: #33415b;
4
5
  display: flex;
5
6
  flex-direction: column;
6
7
  justify-content: space-between;
7
8
 
8
9
  .full-box {
9
- padding: 7px;
10
+ padding: 3px 6px;
10
11
  display: flex;
11
12
  flex-direction: row;
12
13
  align-items: center;
@@ -24,16 +25,14 @@
24
25
  justify-content: space-between;
25
26
  cursor: pointer;
26
27
  margin: 0 10px;
27
- color: rgb(219, 221, 225);
28
- border-radius: 5px;
28
+ border-radius: 4px;
29
29
  }
30
30
 
31
- .link-name {
32
- margin-top: 30px;
31
+ .sidebar-section-hint {
32
+ // margin-top: 30px;
33
33
  margin-left: 10px;
34
- color: rgb(154, 160, 172);
35
34
  font-size: 12px;
36
- text-decoration: none;
35
+ color: #99a0ad;
37
36
  }
38
37
 
39
38
  .new-btn {
@@ -44,14 +43,21 @@
44
43
 
45
44
  .utils {
46
45
  display: flex;
46
+ gap: 8px;
47
47
  }
48
48
  .utilities {
49
- margin-right: 3px;
49
+ // margin-right: 3px;
50
+ transform: scale(1.2);
50
51
  }
51
52
 
52
53
  .file-or-directory:hover,
53
54
  .settings:hover {
54
- background-color: rgb(74, 84, 105);
55
+ background-color: #47546b;
56
+ }
57
+
58
+ .active-file,
59
+ .active-file:hover {
60
+ background-color: #202e46;
55
61
  }
56
62
 
57
63
  .name {
@@ -71,13 +77,12 @@
71
77
 
72
78
  .settings {
73
79
  height: 30px;
74
- color: rgb(219, 221, 225);
75
80
  cursor: pointer;
76
81
  display: flex;
77
82
  align-items: center;
78
83
  justify-content: center;
79
84
  padding: 14px 0;
80
85
  margin: 10px;
81
- border-radius: 5px;
86
+ border-radius: 4px;
82
87
  }
83
88
  }
@@ -5,4 +5,7 @@ import Form from 'react-bootstrap/cjs/Form.js';
5
5
  import Button from 'react-bootstrap/cjs/Button.js';
6
6
  import Modal from 'react-bootstrap/cjs/Modal.js';
7
7
 
8
+ // Pull in custom CSS from vizhub-ui.
9
+ import 'vizhub-ui/dist/vizhub-ui.css';
10
+
8
11
  export { Form, Button, Modal };
@@ -1,6 +1,7 @@
1
1
  :root {
2
2
  --vzcode-font-family: ui-monospace, SFMono-Regular,
3
- SF Mono, Menlo, Consolas, Liberation Mono, monospace;
3
+ 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace !important;
4
+ /* Variable for the background color */
4
5
  --vz-bg-color: rgb(
5
6
  41,
6
7
  44,
@@ -36,7 +37,9 @@ body,
36
37
 
37
38
  .app {
38
39
  background-color: var(--vz-bg-color);
39
- font-family: 'Poppins', sans-serif;
40
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI',
41
+ 'Noto Sans', Helvetica, Arial, sans-serif,
42
+ 'Apple Color Emoji', 'Segoe UI Emoji';
40
43
  display: flex;
41
44
 
42
45
  /* To support Resizer, which is position: absolute */
@@ -0,0 +1,74 @@
1
+ import { useCallback } from 'react';
2
+ import { ThemeLabel } from './themes';
3
+ import { FileId } from '../types';
4
+ import { 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
+ (fileId: FileId) => {
23
+ dispatch({
24
+ type: 'open_tab',
25
+ fileId,
26
+ });
27
+ },
28
+ [dispatch],
29
+ );
30
+
31
+ const closeTabs = useCallback(
32
+ (idsToDelete: Array<FileId>) => {
33
+ dispatch({
34
+ type: 'close_tabs',
35
+ idsToDelete,
36
+ });
37
+ },
38
+ [dispatch],
39
+ );
40
+
41
+ const setTheme = useCallback(
42
+ (themeLabel: ThemeLabel) => {
43
+ dispatch({
44
+ type: 'set_theme',
45
+ themeLabel: themeLabel,
46
+ });
47
+ },
48
+ [dispatch],
49
+ );
50
+
51
+ // True to show the settings modal.
52
+ const setIsSettingsOpen = useCallback(
53
+ (value: boolean) => {
54
+ dispatch({
55
+ type: 'set_is_settings_open',
56
+ value: value,
57
+ });
58
+ },
59
+ [dispatch],
60
+ );
61
+
62
+ const closeSettings = useCallback(() => {
63
+ setIsSettingsOpen(false);
64
+ }, [setIsSettingsOpen]);
65
+
66
+ return {
67
+ setActiveFileId,
68
+ openTab,
69
+ closeTabs,
70
+ setTheme,
71
+ setIsSettingsOpen,
72
+ closeSettings,
73
+ };
74
+ };
@@ -0,0 +1,101 @@
1
+ import { useCallback } from 'react';
2
+ import { FileId, VZCodeContent } from '../types';
3
+ import { randomId } from '../randomId';
4
+
5
+ // CRUD operations for files and directories
6
+ // CRUD = Create, Read, Update, Delete
7
+ export const useFileCRUD = ({
8
+ submitOperation,
9
+ closeTabs,
10
+ }) => {
11
+ // Create a new file
12
+ const createFile = useCallback(() => {
13
+ // TODO use Bootstrap modals not native prompts
14
+ // See https://github.com/vizhub-core/vzcode/issues/252
15
+ const name = prompt('Enter new file name');
16
+ if (name) {
17
+ submitOperation((document: VZCodeContent) => ({
18
+ ...document,
19
+ files: {
20
+ ...document.files,
21
+ [randomId()]: { name, text: '' },
22
+ },
23
+ }));
24
+ }
25
+ }, [submitOperation]);
26
+
27
+ // Called when a file in the sidebar is renamed.
28
+ const handleRenameFileClick = useCallback(
29
+ (fileId: FileId, newName: string) => {
30
+ submitOperation((document) => ({
31
+ ...document,
32
+ files: {
33
+ ...document.files,
34
+ [fileId]: {
35
+ ...document.files[fileId],
36
+ name: newName,
37
+ },
38
+ },
39
+ }));
40
+ },
41
+ [submitOperation],
42
+ );
43
+
44
+ // Deletes a file
45
+ const deleteFile = useCallback(
46
+ (fileId: FileId) => {
47
+ closeTabs([fileId]);
48
+ submitOperation((document) => {
49
+ const updatedFiles = { ...document.files };
50
+ delete updatedFiles[fileId];
51
+ return { ...document, files: updatedFiles };
52
+ });
53
+ },
54
+ [submitOperation, closeTabs],
55
+ );
56
+
57
+ // Deletes a directory and all files within it
58
+ const deleteDirectory = useCallback(
59
+ (path: FileId) => {
60
+ const tabsToClose: Array<FileId> = [];
61
+ submitOperation((document: VZCodeContent) => {
62
+ const updatedFiles = { ...document.files };
63
+ for (const key in updatedFiles) {
64
+ if (updatedFiles[key].name.includes(path)) {
65
+ tabsToClose.push(key);
66
+ delete updatedFiles[key];
67
+ }
68
+ }
69
+ return { ...document, files: updatedFiles };
70
+ });
71
+ closeTabs(tabsToClose);
72
+ },
73
+ [submitOperation, closeTabs],
74
+ );
75
+
76
+ // Generic delete function that calls either deleteFile or deleteDirectory
77
+ // TODO consider if there is a cleaner way to do this
78
+ // - Ideally we would have a separate function for deleting files and directories
79
+ // - When we click the delete button, we should be able to tell if it is a file or directory
80
+ const handleDeleteClick = useCallback(
81
+ (key: string) => {
82
+ // Regex to identify if the key is a file path or a file id.
83
+ if (/^[0-9]*$/.test(key)) {
84
+ if (key.length == 8) {
85
+ deleteFile(key);
86
+ } else {
87
+ deleteDirectory(key);
88
+ }
89
+ } else {
90
+ deleteDirectory(key);
91
+ }
92
+ },
93
+ [deleteFile, deleteDirectory],
94
+ );
95
+
96
+ return {
97
+ createFile,
98
+ handleRenameFileClick,
99
+ handleDeleteClick,
100
+ };
101
+ };
@@ -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
+ };
@@ -1,17 +1,27 @@
1
1
  import { FileId } from '../types';
2
- import { act } from 'react-dom/test-utils';
2
+ import { ThemeLabel } from './themes';
3
3
 
4
- export const reducer = (state, action) => {
5
- switch (action.type) {
6
- // TODO phase this out
4
+ export type VZState = {
5
+ tabList: Array<FileId>;
6
+ activeFileId: FileId | null;
7
+ theme: ThemeLabel;
8
+ isSettingsOpen: boolean;
9
+ };
7
10
 
8
- // The ordered list of tabs.
9
- // TODO make this a URL param
10
- case 'set_tab_list': {
11
- return { ...state, tabList: action.tabList };
12
- }
13
- // TODO make this a URL param
14
- case 'set_active_fileId': {
11
+ export type VZAction =
12
+ | { type: 'set_active_file_id'; activeFileId: FileId }
13
+ | { type: 'open_tab'; fileId: FileId }
14
+ | { type: 'close_tab'; fileIdToRemove: FileId }
15
+ | { type: 'close_tabs'; idsToDelete: Array<FileId> }
16
+ | { type: 'set_theme'; themeLabel: ThemeLabel }
17
+ | { type: 'set_is_settings_open'; value: boolean };
18
+
19
+ export const vzReducer = (
20
+ state: VZState,
21
+ action: VZAction,
22
+ ) => {
23
+ switch (action.type) {
24
+ case 'set_active_file_id': {
15
25
  return {
16
26
  ...state,
17
27
  activeFileId: action.activeFileId,
@@ -33,7 +43,8 @@ export const reducer = (state, action) => {
33
43
  }
34
44
  case 'close_tab': {
35
45
  const i = state.tabList.findIndex(
36
- (fileId) => fileId === action.fileIdToRemove,
46
+ (fileId: FileId) =>
47
+ fileId === action.fileIdToRemove,
37
48
  );
38
49
  if (i != -1) {
39
50
  const newTabList = [
@@ -68,52 +79,40 @@ export const reducer = (state, action) => {
68
79
  ...state,
69
80
  };
70
81
  }
71
- case 'multi_close_tab': {
72
- let newTabList = [...state.tabList]; // Create a copy of the tabList array.
73
- let newActiveFileId: FileId = state.activeFileId;
74
- action.idsToDelete.forEach((id) => {
75
- const i = newTabList.findIndex(
76
- (fileId) => fileId === id,
77
- );
78
- if (i !== -1) {
79
- // Remove the tab from the tab list.
80
- newTabList = [
81
- ...newTabList.slice(0, i),
82
- ...newTabList.slice(i + 1),
83
- ];
84
- // If we are closing the active file,
85
- if (newActiveFileId === id) {
86
- // set the new active file to the next tab over,
87
- if (newTabList.length > 0) {
88
- newActiveFileId =
89
- i === 0 ? newTabList[i] : newTabList[i - 1];
90
- } else {
91
- // or clear out the active file if we've closed the last tab.
92
- newActiveFileId = null;
93
- }
94
- }
95
- }
96
- });
82
+ case 'close_tabs': {
83
+ const newTabList = state.tabList.filter(
84
+ (fileId: FileId) =>
85
+ !action.idsToDelete.includes(fileId),
86
+ );
87
+
88
+ let newActiveFileId: FileId | null =
89
+ state.activeFileId;
90
+
91
+ if (action.idsToDelete.includes(newActiveFileId)) {
92
+ const index =
93
+ state.tabList.indexOf(newActiveFileId);
94
+ newActiveFileId = newTabList[index]
95
+ ? newTabList[index]
96
+ : newTabList[index - 1] || null;
97
+ }
98
+
97
99
  return {
98
100
  ...state,
99
101
  tabList: newTabList,
100
102
  activeFileId: newActiveFileId,
101
103
  };
102
104
  }
103
- // The current theme.
104
- // TODO persist this in local storage
105
- case 'set_Theme': {
105
+ case 'set_theme': {
106
106
  return {
107
107
  ...state,
108
108
  theme: action.themeLabel,
109
109
  };
110
110
  }
111
- case 'set_Is_Settings_Open': {
111
+ case 'set_is_settings_open': {
112
112
  return {
113
113
  ...state,
114
114
  isSettingsOpen: action.value,
115
115
  };
116
116
  }
117
117
  }
118
- throw Error('Unknown action: ' + action.type);
119
118
  };