Compare commits

...

4 Commits

Author SHA1 Message Date
Jixun Wu 3a2a31f372 test: fix sanity check test 2023-06-03 14:15:53 +01:00
Jixun Wu d91b71e0d3 chore: fix typo 2023-06-03 14:15:21 +01:00
Jixun Wu 4def7a260e refactor: move components to sub dir 2023-06-03 14:14:50 +01:00
Jixun Wu 4602f96260 feat: setup redux store for settings 2023-06-03 14:09:11 +01:00
17 changed files with 158 additions and 22 deletions

View File

@ -23,7 +23,9 @@
"@jixun/libparakeet": "0.1.1",
"@reduxjs/toolkit": "^1.9.5",
"framer-motion": "^10.12.16",
"immer": "^10.0.2",
"nanoid": "^4.0.2",
"radash": "^10.8.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-dropzone": "^14.2.3",

View File

@ -22,9 +22,15 @@ dependencies:
framer-motion:
specifier: ^10.12.16
version: 10.12.16(react-dom@18.2.0)(react@18.2.0)
immer:
specifier: ^10.0.2
version: 10.0.2
nanoid:
specifier: ^4.0.2
version: 4.0.2
radash:
specifier: ^10.8.1
version: 10.8.1
react:
specifier: ^18.2.0
version: 18.2.0
@ -5483,6 +5489,11 @@ packages:
engines: { node: '>= 4' }
dev: true
/immer@10.0.2:
resolution:
{ integrity: sha512-Rx3CqeqQ19sxUtYV9CU911Vhy8/721wRFnJv3REVGWUmoAcIwzifTsdmJte/MV+0/XpM35LZdQMBGkRIoLPwQA== }
dev: false
/immer@9.0.21:
resolution:
{ integrity: sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA== }
@ -6608,6 +6619,12 @@ packages:
{ integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== }
dev: true
/radash@10.8.1:
resolution:
{ integrity: sha512-NzYo3XgM9Tzjf5iFPIMG2l5+LSOCi2H7Axe3Ry/1PrhlvuqxUoiLsmcTBtw4CfKtzy5Fzo79STiEj9JZWMfDQg== }
engines: { node: '>=14.18.0' }
dev: false
/randombytes@2.1.0:
resolution:
{ integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== }

View File

@ -1,5 +1,5 @@
import { renderWithProviders, screen, waitFor } from '~/test-utils/test-helper';
import App from '~/App';
import App from '~/components/App';
vi.mock('../decrypt-worker/client', () => {
return {

View File

@ -1,7 +1,7 @@
import { Box, Center, Container } from '@chakra-ui/react';
import { SelectFile } from './SelectFile';
import { FileListing } from './features/file-listing/FileListing';
import { FileListing } from '~/features/file-listing/FileListing';
import { Footer } from './Footer';
function App() {

View File

@ -0,0 +1,23 @@
import { useEffect } from 'react';
import App from './App';
import { ChakraProvider } from '@chakra-ui/react';
import { Provider } from 'react-redux';
import { theme } from '~/theme';
import { persistSettings } from '~/features/settings/persistSettings';
import { setupStore } from '~/store';
// Private to this file only.
const store = setupStore();
export function AppRoot() {
useEffect(() => persistSettings(store), []);
return (
<ChakraProvider theme={theme}>
<Provider store={store}>
<App />
</Provider>
</ChakraProvider>
);
}

View File

@ -2,8 +2,8 @@ import { useDropzone } from 'react-dropzone';
import { Box, Text } from '@chakra-ui/react';
import { UnlockIcon } from '@chakra-ui/icons';
import { useAppDispatch } from './hooks';
import { addNewFile, processFile } from './features/file-listing/fileListingSlice';
import { useAppDispatch } from '~/hooks';
import { addNewFile, processFile } from '~/features/file-listing/fileListingSlice';
import { nanoid } from 'nanoid';
export function SelectFile() {

View File

@ -1,7 +1,7 @@
import { VStack } from '@chakra-ui/react';
import { selectFiles } from './fileListingSlice';
import { useAppSelector } from '../../hooks';
import { useAppSelector } from '~/hooks';
import { FileRow } from './FileRow';
export function FileListing() {

View File

@ -0,0 +1,45 @@
import { debounce } from 'radash';
import { produce } from 'immer';
import type { AppStore } from '~/store';
import { SettingsState, settingsSlice, updateSettings } from './settingsSlice';
import { enumObject } from '~/util/objects';
import { getLogger } from '~/util/logUtils';
const DEFAULT_STORAGE_KEY = 'um-react-settings';
function mergeSettings(settings: SettingsState): SettingsState {
return produce(settingsSlice.getInitialState(), (draft) => {
for (const [k, v] of enumObject(settings.qmc2?.keys)) {
if (typeof v === 'string') {
draft.qmc2.keys[k] = v;
}
}
});
}
export function persistSettings(store: AppStore, storageKey = DEFAULT_STORAGE_KEY) {
let lastSettings: unknown;
try {
const loadedSettings: SettingsState = JSON.parse(localStorage.getItem(storageKey) ?? '');
if (loadedSettings) {
const mergedSettings = mergeSettings(loadedSettings);
store.dispatch(updateSettings(mergedSettings));
getLogger().debug('settings loaded');
}
} catch {
// load failed, ignore.
}
return store.subscribe(
debounce({ delay: 150 }, () => {
const currentSettings = store.getState().settings;
if (lastSettings !== currentSettings) {
lastSettings = currentSettings;
localStorage.setItem(storageKey, JSON.stringify(currentSettings));
getLogger().debug('settings saved');
}
})
);
}

View File

@ -0,0 +1,31 @@
import { createSlice } from '@reduxjs/toolkit';
import type { PayloadAction } from '@reduxjs/toolkit';
export interface QMCSettings {
keys: Record<string, string>; // { [fileName]: ekey }
}
export interface SettingsState {
qmc2: QMCSettings;
}
const initialState: SettingsState = {
qmc2: { keys: {} },
};
export const settingsSlice = createSlice({
name: 'settings',
initialState,
reducers: {
updateSettings: (_state, { payload }: PayloadAction<SettingsState>) => {
return payload;
},
resetConfig: () => {
return initialState;
},
},
});
export const { updateSettings, resetConfig } = settingsSlice.actions;
export default settingsSlice.reducer;

View File

@ -1,21 +1,10 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { ChakraProvider } from '@chakra-ui/react';
import { Provider } from 'react-redux';
import { setupStore } from './store';
import { theme } from './theme';
// Private to this file only.
const store = setupStore();
import { AppRoot } from './components/AppRoot';
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<ChakraProvider theme={theme}>
<Provider store={store}>
<App />
</Provider>
</ChakraProvider>
<AppRoot />
</React.StrictMode>
);

View File

@ -1,8 +1,10 @@
import { PreloadedState, combineReducers, configureStore } from '@reduxjs/toolkit';
import fileListingReducer from './features/file-listing/fileListingSlice';
import settingsReducer from './features/settings/settingsSlice';
const rootReducer = combineReducers({
fileListing: fileListingReducer,
settings: settingsReducer,
});
export const setupStore = (preloadedState?: PreloadedState<RootState>) =>

View File

@ -3,7 +3,7 @@ import { nextTickAsync } from '../nextTick';
class SimpleQueue<T, R = void> extends ConcurrentQueue<T> {
handler(_item: T): Promise<R> {
throw new Error('Method not overriden');
throw new Error('Method not overridden');
}
}
@ -39,7 +39,7 @@ test('should be able to process the queue within limit', async () => {
await promises[i];
}
// Wait till all fullfilled
// Wait till all fulfilled
while (queuedResolver.length !== 5) {
await nextTickAsync();
}
@ -85,13 +85,13 @@ test('it should move on to the next item in the queue once failed', async () =>
promises.push(queue.add(4));
promises.push(queue.add(5));
// Let first 2 be fullfilled
// Let first 2 be fulfilled
for (let i = 0; i < 2; i++) {
queuedResolver[i]();
await promises[i];
}
// Wait till all fullfilled
// Wait till all fulfilled
while (queuedResolver.length !== 4) {
await nextTickAsync();
}

View File

@ -23,3 +23,23 @@ export function withGroupedLogs<R = unknown>(label: string, fn: () => R): R {
);
}
}
const noop = (..._args: unknown[]) => {
// noop
};
const dummyLogger = {
log: noop,
info: noop,
warn: noop,
debug: noop,
trace: noop,
};
export function getLogger() {
if (import.meta.env.ENABLE_PERF_LOG === '1') {
return window.console;
} else {
return dummyLogger;
}
}

7
src/util/objects.ts Normal file
View File

@ -0,0 +1,7 @@
export function* enumObject<T>(obj: Record<string, T> | null | void): Generator<[string, T]> {
if (obj && typeof obj === 'object') {
for (const key in obj) {
yield [key, obj[key]];
}
}
}