mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-12 09:36:27 +00:00
explorer/reducers: don't use FileMetadata
This separates the explorer files state from the fileStorage FileMetadata. The latter contains extra info that the explorer doesn't need. This will prevent rerendering each time the file contents change.
This commit is contained in:
@@ -6,7 +6,6 @@ import { cleanup } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
import { testRender, uuid } from '../../test';
|
||||
import { FileMetadata } from '../fileStorage/actions';
|
||||
import Explorer from './Explorer';
|
||||
import {
|
||||
explorerArchiveAllFiles,
|
||||
@@ -15,6 +14,7 @@ import {
|
||||
explorerImportFiles,
|
||||
explorerRenameFile,
|
||||
} from './actions';
|
||||
import { ExplorerFileInfo } from './reducers';
|
||||
|
||||
afterEach(async () => {
|
||||
jest.restoreAllMocks();
|
||||
@@ -22,10 +22,9 @@ afterEach(async () => {
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
const testFile: FileMetadata = {
|
||||
uuid: uuid(0),
|
||||
path: 'test.file',
|
||||
sha256: '',
|
||||
const testFile: ExplorerFileInfo = {
|
||||
id: uuid(0),
|
||||
name: 'test.file',
|
||||
};
|
||||
|
||||
describe('archive button', () => {
|
||||
|
||||
@@ -290,12 +290,12 @@ const FileTree: React.VoidFunctionComponent<FileTreeProps> = ({ i18n }) => {
|
||||
() =>
|
||||
files.reduce(
|
||||
(obj, file) => {
|
||||
const index = file.uuid;
|
||||
const index = file.id;
|
||||
|
||||
obj[index] = {
|
||||
index,
|
||||
data: {
|
||||
fileName: file.path,
|
||||
fileName: file.name,
|
||||
icon: 'document',
|
||||
secondaryLabel: (
|
||||
<TreeItemContext.Consumer>
|
||||
@@ -319,8 +319,8 @@ const FileTree: React.VoidFunctionComponent<FileTreeProps> = ({ i18n }) => {
|
||||
hasChildren: true,
|
||||
children: [...files]
|
||||
// REVISIT: consider using Intl.Collator() for i18n.locale
|
||||
.sort((a, b) => a.path.localeCompare(b.path))
|
||||
.map((n) => n.uuid),
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((n) => n.id),
|
||||
},
|
||||
} as Record<TreeItemIndex, FileTreeItem>,
|
||||
),
|
||||
|
||||
@@ -45,7 +45,7 @@ const NewFileWizard: React.VoidFunctionComponent<NewFileWizardProps> = ({
|
||||
const fileNameValidation = validateFileName(
|
||||
fileName,
|
||||
pythonFileExtension,
|
||||
files.map((f) => f.path),
|
||||
files.map((f) => f.name),
|
||||
);
|
||||
const [hubType, setHubType] = useState(defaultHub);
|
||||
|
||||
|
||||
@@ -9,50 +9,84 @@ import {
|
||||
fileStorageDidInitialize,
|
||||
fileStorageDidRemoveItem,
|
||||
} from '../fileStorage/actions';
|
||||
import reducers from './reducers';
|
||||
import reducers, { ExplorerFileInfo } from './reducers';
|
||||
|
||||
type State = ReturnType<typeof reducers>;
|
||||
|
||||
test('files', () => {
|
||||
const testFile: FileMetadata = {
|
||||
describe('files', () => {
|
||||
const testFile: ExplorerFileInfo = {
|
||||
id: uuid(0),
|
||||
name: 'test.file',
|
||||
};
|
||||
|
||||
const testFileMetadata: FileMetadata = {
|
||||
uuid: uuid(0),
|
||||
path: 'test.file',
|
||||
sha256: '',
|
||||
};
|
||||
|
||||
const modifiedFile: FileMetadata = { ...testFile, path: 'modified.file' };
|
||||
const modifiedFile: ExplorerFileInfo = {
|
||||
...testFile,
|
||||
name: 'modified.file',
|
||||
};
|
||||
|
||||
expect(testFile).not.toEqual(modifiedFile);
|
||||
const modifiedFileMetadata: FileMetadata = {
|
||||
...testFileMetadata,
|
||||
path: 'modified.file',
|
||||
};
|
||||
|
||||
// initialization populates file list
|
||||
expect(
|
||||
reducers(
|
||||
{ files: [] as readonly FileMetadata[] } as State,
|
||||
fileStorageDidInitialize([testFile]),
|
||||
).files,
|
||||
).toEqual([testFile]);
|
||||
beforeAll(() => {
|
||||
// check validity of test data before starting tests
|
||||
expect(testFile).not.toEqual(modifiedFile);
|
||||
expect(testFileMetadata).not.toEqual(modifiedFileMetadata);
|
||||
});
|
||||
|
||||
// adding appends an item
|
||||
expect(
|
||||
reducers(
|
||||
{ files: [] as readonly FileMetadata[] } as State,
|
||||
fileStorageDidAddItem(testFile),
|
||||
).files,
|
||||
).toEqual([testFile]);
|
||||
it('should get a list when file storage is initialized', () => {
|
||||
expect(
|
||||
reducers(
|
||||
{ files: [] as readonly ExplorerFileInfo[] } as State,
|
||||
fileStorageDidInitialize([testFileMetadata]),
|
||||
).files,
|
||||
).toEqual([testFile]);
|
||||
});
|
||||
|
||||
// changing replaces an item
|
||||
expect(
|
||||
reducers(
|
||||
{ files: [testFile] as readonly FileMetadata[] } as State,
|
||||
fileStorageDidChangeItem(testFile, modifiedFile),
|
||||
).files,
|
||||
).toEqual([modifiedFile]);
|
||||
it('should modify the list when a file is added to storage', () => {
|
||||
expect(
|
||||
reducers(
|
||||
{ files: [] as readonly ExplorerFileInfo[] } as State,
|
||||
fileStorageDidAddItem(testFileMetadata),
|
||||
).files,
|
||||
).toEqual([testFile]);
|
||||
});
|
||||
|
||||
// removing deletes an item
|
||||
expect(
|
||||
reducers(
|
||||
{ files: [testFile] as readonly FileMetadata[] } as State,
|
||||
fileStorageDidRemoveItem(testFile),
|
||||
).files,
|
||||
).not.toContain(testFile);
|
||||
it('should modify the list when an item is renamed in storage', () => {
|
||||
expect(
|
||||
reducers(
|
||||
{ files: [testFile] as readonly ExplorerFileInfo[] } as State,
|
||||
fileStorageDidChangeItem(testFileMetadata, modifiedFileMetadata),
|
||||
).files,
|
||||
).toEqual([modifiedFile]);
|
||||
});
|
||||
|
||||
it('should not modify the list if a change is made other than renaming', () => {
|
||||
const originalList: readonly ExplorerFileInfo[] = [testFile];
|
||||
expect(
|
||||
reducers(
|
||||
{ files: originalList } as State,
|
||||
fileStorageDidChangeItem(testFileMetadata, {
|
||||
...testFileMetadata,
|
||||
sha256: 'changed',
|
||||
}),
|
||||
).files,
|
||||
).toBe(originalList);
|
||||
});
|
||||
|
||||
it('should modify the list when a file is removed from storage', () => {
|
||||
expect(
|
||||
reducers(
|
||||
{ files: [testFile] as readonly ExplorerFileInfo[] } as State,
|
||||
fileStorageDidRemoveItem(testFileMetadata),
|
||||
).files,
|
||||
).not.toContain(testFile);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,21 +12,40 @@ import {
|
||||
|
||||
import renameFileDialog from './renameFileDialog/reducers';
|
||||
|
||||
const files: Reducer<readonly FileMetadata[]> = (state = [], action) => {
|
||||
export type ExplorerFileInfo = Readonly<{
|
||||
/** A unique identifier for this file (not the path, which can change). */
|
||||
id: string;
|
||||
/** The file name (including extension - without directory). */
|
||||
name: string;
|
||||
}>;
|
||||
|
||||
function metadataToInfo(file: FileMetadata): ExplorerFileInfo {
|
||||
return { id: file.uuid, name: file.path };
|
||||
}
|
||||
|
||||
const files: Reducer<readonly ExplorerFileInfo[]> = (state = [], action) => {
|
||||
if (fileStorageDidInitialize.matches(action)) {
|
||||
return [...action.files];
|
||||
return action.files.map(metadataToInfo);
|
||||
}
|
||||
|
||||
if (fileStorageDidAddItem.matches(action)) {
|
||||
return [...state, action.file];
|
||||
return [...state, metadataToInfo(action.file)];
|
||||
}
|
||||
|
||||
if (fileStorageDidChangeItem.matches(action)) {
|
||||
return [...state].map((f) => (f.uuid === action.file.uuid ? action.file : f));
|
||||
// We only care about UUID and the path. UUID can't change so if the
|
||||
// path didn't change, there is nothing to do.
|
||||
if (action.oldFile.path === action.file.path) {
|
||||
return state;
|
||||
}
|
||||
|
||||
return state.map((f) =>
|
||||
f.id === action.file.uuid ? metadataToInfo(action.file) : f,
|
||||
);
|
||||
}
|
||||
|
||||
if (fileStorageDidRemoveItem.matches(action)) {
|
||||
return [...state].filter((value) => value.uuid !== action.file.uuid);
|
||||
return state.filter((value) => value.id !== action.file.uuid);
|
||||
}
|
||||
|
||||
return state;
|
||||
|
||||
@@ -28,7 +28,7 @@ const RenameFileDialog: React.VFC = () => {
|
||||
const result = validateFileName(
|
||||
newName,
|
||||
extension,
|
||||
files.map((f) => f.path),
|
||||
files.map((f) => f.name),
|
||||
);
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
@@ -125,7 +125,7 @@ function* handleExplorerImportFiles(): Generator {
|
||||
const result = validateFileName(
|
||||
baseName,
|
||||
pythonFileExtension,
|
||||
existingFiles.map((f) => f.path),
|
||||
existingFiles.map((f) => f.name),
|
||||
);
|
||||
|
||||
if (result != FileNameValidationResult.IsOk) {
|
||||
|
||||
Reference in New Issue
Block a user