Fix tune loading and routing (#378)

This commit is contained in:
Piotr Rogowski 2022-01-16 18:32:55 +01:00 committed by GitHub
parent affdae49e8
commit c53cd0dcc3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 188 additions and 35 deletions

View File

@ -18,7 +18,6 @@ import {
useEffect, useEffect,
useMemo, useMemo,
} from 'react'; } from 'react';
import useBrowserStorage from './hooks/useBrowserStorage';
import TopBar from './components/TopBar'; import TopBar from './components/TopBar';
import StatusBar from './components/StatusBar'; import StatusBar from './components/StatusBar';
import { Routes } from './routes'; import { Routes } from './routes';
@ -35,6 +34,7 @@ import {
} from './types/state'; } from './types/state';
import useDb from './hooks/useDb'; import useDb from './hooks/useDb';
import useServerStorage from './hooks/useServerStorage'; import useServerStorage from './hooks/useServerStorage';
import Info from './pages/Info';
// TODO: fix this // TODO: fix this
// lazy loading this component causes a weird Curve canvas scaling // lazy loading this component causes a weird Curve canvas scaling
@ -59,7 +59,6 @@ const App = ({ ui, navigation }: { ui: UIState, navigation: NavigationState }) =
const margin = ui.sidebarCollapsed ? 80 : 250; const margin = ui.sidebarCollapsed ? 80 : 250;
const { getTune } = useDb(); const { getTune } = useDb();
const { getFile } = useServerStorage(); const { getFile } = useServerStorage();
const { storageSet } = useBrowserStorage();
// const [lastDialogPath, setLastDialogPath] = useState<string|null>(); // const [lastDialogPath, setLastDialogPath] = useState<string|null>();
// const lastDialogPath = storageGetSync('lastDialog'); // const lastDialogPath = storageGetSync('lastDialog');
@ -69,21 +68,21 @@ const App = ({ ui, navigation }: { ui: UIState, navigation: NavigationState }) =
path: Routes.TUNE_ROOT, path: Routes.TUNE_ROOT,
}), [pathname]); }), [pathname]);
useEffect(() => {
const tuneId = (matchedTunePath?.params as any)?.tuneId; const tuneId = (matchedTunePath?.params as any)?.tuneId;
if (tuneId) {
useEffect(() => {
if (tuneId) {
getTune(tuneId).then(async (tuneData) => { getTune(tuneId).then(async (tuneData) => {
const [tuneRaw, iniRaw] = await Promise.all([ const [tuneRaw, iniRaw] = await Promise.all([
await getFile(tuneData.tuneFile!), getFile(tuneData.tuneFile!),
await getFile(tuneData.customIniFile!), getFile(tuneData.customIniFile!),
]); ]);
store.dispatch({ type: 'tuneData/load', payload: tuneData });
loadTune(tuneRaw, iniRaw); loadTune(tuneRaw, iniRaw);
}); });
storageSet('lastTuneId', tuneId);
store.dispatch({ type: 'navigation/tuneId', payload: tuneId }); store.dispatch({ type: 'navigation/tuneId', payload: tuneId });
} }
@ -96,7 +95,7 @@ const App = ({ ui, navigation }: { ui: UIState, navigation: NavigationState }) =
// }; // };
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, [tuneId]);
const ContentFor = useCallback((props: { children: ReactNode, marginLeft?: number }) => { const ContentFor = useCallback((props: { children: ReactNode, marginLeft?: number }) => {
const { children, marginLeft } = props; const { children, marginLeft } = props;
@ -127,9 +126,6 @@ const App = ({ ui, navigation }: { ui: UIState, navigation: NavigationState }) =
<TopBar tuneId={navigation.tuneId} /> <TopBar tuneId={navigation.tuneId} />
<Switch> <Switch>
<Route path={Routes.ROOT} exact> <Route path={Routes.ROOT} exact>
{/* <Route path={Routes.ROOT} exact>
<Redirect to={lastDialogPath || Routes.TUNE_ROOT} />
</Route> */}
<ContentFor> <ContentFor>
<Result <Result
status="info" status="info"
@ -138,6 +134,11 @@ const App = ({ ui, navigation }: { ui: UIState, navigation: NavigationState }) =
/> />
</ContentFor> </ContentFor>
</Route> </Route>
<Route path={Routes.TUNE_ROOT} exact>
<ContentFor>
<Info />
</ContentFor>
</Route>
<Route path={Routes.TUNE_TUNE}> <Route path={Routes.TUNE_TUNE}>
<ContentFor marginLeft={margin}> <ContentFor marginLeft={margin}>
<Tune /> <Tune />

View File

@ -86,7 +86,7 @@ const SideBar = ({
const { isConfigReady } = useConfig(config); const { isConfigReady } = useConfig(config);
const checkCondition = useCallback((condition: string) => evaluateExpression(condition, tune.constants, config), [tune.constants, config]); const checkCondition = useCallback((condition: string) => evaluateExpression(condition, tune.constants, config), [tune.constants, config]);
const buildUrl = useCallback((main: string, sub: string) => generatePath(Routes.TUNE_DIALOG, { const buildUrl = useCallback((main: string, sub: string) => generatePath(Routes.TUNE_DIALOG, {
tuneId: navigation.tuneId || 'not-ready', tuneId: navigation.tuneId!,
category: main, category: main,
dialog: sub, dialog: sub,
}), [navigation.tuneId]); }), [navigation.tuneId]);

View File

@ -39,6 +39,7 @@ import {
FundOutlined, FundOutlined,
UserAddOutlined, UserAddOutlined,
LogoutOutlined, LogoutOutlined,
InfoCircleOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { import {
useCallback, useCallback,
@ -69,6 +70,9 @@ const TopBar = ({ tuneId }: { tuneId: string | null }) => {
const { currentUser, logout } = useAuth(); const { currentUser, logout } = useAuth();
const history = useHistory(); const history = useHistory();
const buildTuneUrl = (route: string) => tuneId ? generatePath(route, { tuneId }) : null; const buildTuneUrl = (route: string) => tuneId ? generatePath(route, { tuneId }) : null;
const matchedTuneRootPath = useMemo(() => matchPath(pathname, {
path: Routes.TUNE_ROOT,
}), [pathname]);
const matchedTabPath = useMemo(() => matchPath(pathname, { const matchedTabPath = useMemo(() => matchPath(pathname, {
path: Routes.TUNE_TAB, path: Routes.TUNE_TAB,
}), [pathname]); }), [pathname]);
@ -106,11 +110,17 @@ const TopBar = ({ tuneId }: { tuneId: string | null }) => {
<Col span={10} md={10} sm={16} style={{ textAlign: 'center' }}> <Col span={10} md={10} sm={16} style={{ textAlign: 'center' }}>
<Radio.Group <Radio.Group
key={pathname} key={pathname}
defaultValue={matchedTabPath?.url} defaultValue={matchedTabPath?.url || matchedTuneRootPath?.url}
optionType="button" optionType="button"
buttonStyle="solid" buttonStyle="solid"
onChange={(e) => history.push(e.target.value)} onChange={(e) => history.push(e.target.value)}
> >
<Radio.Button value={buildTuneUrl(Routes.TUNE_ROOT)}>
<Space>
<InfoCircleOutlined />
{sm && 'Info'}
</Space>
</Radio.Button>
<Radio.Button value={buildTuneUrl(Routes.TUNE_TUNE)}> <Radio.Button value={buildTuneUrl(Routes.TUNE_TUNE)}>
<Space> <Space>
<ToolOutlined /> <ToolOutlined />

View File

@ -1,5 +1,6 @@
import { notification } from 'antd'; import { notification } from 'antd';
import * as Sentry from '@sentry/browser'; import * as Sentry from '@sentry/browser';
import { Timestamp } from 'firebase/firestore';
import { import {
fireStoreDoc, fireStoreDoc,
getDoc, getDoc,
@ -16,8 +17,13 @@ const useDb = () => {
const getData = async (tuneId: string) => { const getData = async (tuneId: string) => {
try { try {
const tune = (await getDoc(fireStoreDoc(db, TUNES_PATH, tuneId))).data() as TuneDbData; const tune = (await getDoc(fireStoreDoc(db, TUNES_PATH, tuneId))).data() as TuneDbData;
const processed = {
...tune,
createdAt: (tune?.createdAt as Timestamp)?.toDate().toISOString(),
updatedAt: (tune?.updatedAt as Timestamp)?.toDate().toISOString(),
};
return Promise.resolve(tune); return Promise.resolve(processed);
} catch (error) { } catch (error) {
Sentry.captureException(error); Sentry.captureException(error);
console.error(error); console.error(error);

129
src/pages/Info.tsx Normal file
View File

@ -0,0 +1,129 @@
import { connect } from 'react-redux';
import ReactMarkdown from 'react-markdown';
import {
Col,
Divider,
Form,
Input,
Row,
Select,
Skeleton,
} from 'antd';
import {
AppState,
TuneDataState,
} from '../types/state';
const { Item } = Form;
const containerStyle = {
padding: 20,
maxWidth: 600,
margin: '0 auto',
};
const rowProps = { gutter: 10 };
const mapStateToProps = (state: AppState) => ({
tuneData: state.tuneData,
});
const Info = ({ tuneData }: { tuneData: TuneDataState }) => {
if (!tuneData.details) {
return (
<div style={containerStyle}>
<Skeleton active />
</div>
);
}
return (
<div style={containerStyle}>
<Divider>Details</Divider>
<Form>
<Row {...rowProps}>
<Col span={12}>
<Item>
<Input value={tuneData.details.make!} addonBefore="Make" />
</Item>
</Col>
<Col span={12}>
<Item>
<Input value={tuneData.details.model!} addonBefore="Model" />
</Item>
</Col>
</Row>
<Row {...rowProps}>
<Col span={12}>
<Item>
<Input value={tuneData.details.year!} addonBefore="Year" style={{ width: '100%' }} />
</Item>
</Col>
<Col span={12}>
<Item>
<Input value={tuneData.details.displacement!} addonBefore="Displacement" addonAfter="l" />
</Item>
</Col>
</Row>
<Row {...rowProps}>
<Col span={12}>
<Item>
<Input value={tuneData.details.hp!} addonBefore="HP" style={{ width: '100%' }} />
</Item>
</Col>
<Col span={12}>
<Item>
<Input value={tuneData.details.stockHp!} addonBefore="Stock HP" style={{ width: '100%' }} />
</Item>
</Col>
</Row>
<Row {...rowProps}>
<Col span={12}>
<Item>
<Input value={tuneData.details.engineCode!} addonBefore="Engine code" />
</Item>
</Col>
<Col span={12}>
<Item>
<Input value={tuneData.details.cylindersCount!} addonBefore="No of cylinders" style={{ width: '100%' }} />
</Item>
</Col>
</Row>
<Row {...rowProps}>
<Col span={12}>
<Item>
<Select placeholder="Aspiration" style={{ width: '100%' }} value={tuneData.details.aspiration}>
<Select.Option value="na">Naturally aspirated</Select.Option>
<Select.Option value="turbocharger">Turbocharged</Select.Option>
<Select.Option value="supercharger">Supercharged</Select.Option>
</Select>
</Item>
</Col>
<Col span={12}>
<Item>
<Input value={tuneData.details.fuel!} addonBefore="Fuel" />
</Item>
</Col>
</Row>
<Row {...rowProps}>
<Col span={12}>
<Item>
<Input value={tuneData.details.injectorsSize!} addonBefore="Injectors size" addonAfter="cc" />
</Item>
</Col>
<Col span={12}>
<Item>
<Input value={tuneData.details.coils!} addonBefore="Coils" />
</Item>
</Col>
</Row>
</Form>
<Divider>README</Divider>
<div className="markdown-preview" style={{ height: '100%' }}>
{tuneData.details?.readme && <ReactMarkdown>
{`${tuneData.details?.readme}`}
</ReactMarkdown>}
</div>
</div>
);
};
export default connect(mapStateToProps)(Info);

View File

@ -42,7 +42,7 @@ const Tune = ({ navigation, config }: { navigation: NavigationState, config: Con
const firstCategory = Object.keys(config.menus)[0]; const firstCategory = Object.keys(config.menus)[0];
const firstDialog = Object.keys(config.menus[firstCategory].subMenus)[0]; const firstDialog = Object.keys(config.menus[firstCategory].subMenus)[0];
return generatePath(Routes.TUNE_DIALOG, { return generatePath(Routes.TUNE_DIALOG, {
tuneId: navigation.tuneId || 'not-ready', tuneId: navigation.tuneId!,
category: firstCategory, category: firstCategory,
dialog: firstDialog, dialog: firstDialog,
}); });

View File

@ -34,7 +34,10 @@ import * as Sentry from '@sentry/browser';
import { INI } from '@speedy-tuner/ini'; import { INI } from '@speedy-tuner/ini';
import { UploadRequestOption } from 'rc-upload/lib/interface'; import { UploadRequestOption } from 'rc-upload/lib/interface';
import { UploadFile } from 'antd/lib/upload/interface'; import { UploadFile } from 'antd/lib/upload/interface';
import { useHistory } from 'react-router-dom'; import {
generatePath,
useHistory,
} from 'react-router-dom';
import ReactMarkdown from 'react-markdown'; import ReactMarkdown from 'react-markdown';
import pako from 'pako'; import pako from 'pako';
import { import {
@ -47,7 +50,6 @@ import {
} from './auth/notifications'; } from './auth/notifications';
import { useAuth } from '../contexts/AuthContext'; import { useAuth } from '../contexts/AuthContext';
import { Routes } from '../routes'; import { Routes } from '../routes';
import useBrowserStorage from '../hooks/useBrowserStorage';
import TuneParser from '../utils/tune/TuneParser'; import TuneParser from '../utils/tune/TuneParser';
import TriggerLogsParser from '../utils/logs/TriggerLogsParser'; import TriggerLogsParser from '../utils/logs/TriggerLogsParser';
import LogParser from '../utils/logs/LogParser'; import LogParser from '../utils/logs/LogParser';
@ -87,7 +89,6 @@ const containerStyle = {
margin: '0 auto', margin: '0 auto',
}; };
const newTuneIdKey = 'newTuneId';
const maxFileSizeMB = 10; const maxFileSizeMB = 10;
const descriptionEditorHeight = 260; const descriptionEditorHeight = 260;
const rowProps = { gutter: 10 }; const rowProps = { gutter: 10 };
@ -113,7 +114,6 @@ const UploadPage = () => {
const hasNavigatorShare = navigator.share !== undefined; const hasNavigatorShare = navigator.share !== undefined;
const { currentUser, refreshToken } = useAuth(); const { currentUser, refreshToken } = useAuth();
const history = useHistory(); const history = useHistory();
const { storageSet, storageGet, storageDelete } = useBrowserStorage();
const { removeFile, uploadFile, basePathForFile } = useServerStorage(); const { removeFile, uploadFile, basePathForFile } = useServerStorage();
const { updateData, getTune } = useDb(); const { updateData, getTune } = useDb();
const requiredRules = [{ required: true, message: 'This field is required!' }]; const requiredRules = [{ required: true, message: 'This field is required!' }];
@ -121,6 +121,10 @@ const UploadPage = () => {
const noop = () => { }; const noop = () => { };
const goToNewTune = () => history.push(generatePath(Routes.TUNE_ROOT, {
tuneId: newTuneId!,
}));
const copyToClipboard = async () => { const copyToClipboard = async () => {
if (navigator.clipboard) { if (navigator.clipboard) {
await navigator.clipboard.writeText(shareUrl!); await navigator.clipboard.writeText(shareUrl!);
@ -156,7 +160,6 @@ const UploadPage = () => {
}); });
setIsLoading(false); setIsLoading(false);
setIsPublished(true); setIsPublished(true);
storageDelete(newTuneIdKey);
}; };
const validateSize = (file: File) => Promise.resolve({ const validateSize = (file: File) => Promise.resolve({
@ -425,18 +428,12 @@ const UploadPage = () => {
setIsUserAuthorized(true); setIsUserAuthorized(true);
} catch (error) { } catch (error) {
Sentry.captureException(error); Sentry.captureException(error);
storageDelete(newTuneIdKey);
console.error(error); console.error(error);
genericError(error as Error); genericError(error as Error);
} }
let newTuneIdTemp = await storageGet(newTuneIdKey); setNewTuneId(nanoidCustom());
if (!newTuneIdTemp) { }, [currentUser, history, refreshToken]);
newTuneIdTemp = nanoidCustom();
await storageSet(newTuneIdKey, newTuneIdTemp);
}
setNewTuneId(newTuneIdTemp);
}, [currentUser, history, refreshToken, storageDelete, storageGet, storageSet]);
useEffect(() => { useEffect(() => {
prepareData(); prepareData();
@ -480,9 +477,7 @@ const UploadPage = () => {
</Button> : <Button </Button> : <Button
type="primary" type="primary"
block block
onClick={() => { onClick={goToNewTune}
window.location.href = shareUrl as string;
}}
> >
Open Open
</Button>} </Button>}

View File

@ -9,6 +9,7 @@ import {
AppState, AppState,
ConfigState, ConfigState,
LogsState, LogsState,
TuneDataState,
TuneState, TuneState,
UpdateTunePayload, UpdateTunePayload,
} from './types/state'; } from './types/state';
@ -16,6 +17,7 @@ import {
// tune and config // tune and config
const updateTune = createAction<UpdateTunePayload>('tune/update'); const updateTune = createAction<UpdateTunePayload>('tune/update');
const loadTune = createAction<TuneState>('tune/load'); const loadTune = createAction<TuneState>('tune/load');
const loadTuneData = createAction<TuneDataState>('tuneData/load');
const loadConfig = createAction<ConfigState>('config/load'); const loadConfig = createAction<ConfigState>('config/load');
// navigation // navigation
@ -35,6 +37,7 @@ const initialState: AppState = {
tune: { tune: {
constants: {}, constants: {},
}, },
tuneData: {},
logs: [], logs: [],
config: {} as any, config: {} as any,
ui: { ui: {
@ -56,6 +59,9 @@ const rootReducer = createReducer(initialState, (builder) => {
.addCase(loadTune, (state: AppState, action) => { .addCase(loadTune, (state: AppState, action) => {
state.tune = action.payload; state.tune = action.payload;
}) })
.addCase(loadTuneData, (state: AppState, action) => {
state.tuneData = action.payload;
})
.addCase(loadLogs, (state: AppState, action) => { .addCase(loadLogs, (state: AppState, action) => {
state.logs = action.payload; state.logs = action.payload;
}) })

View File

@ -1,3 +1,5 @@
import { Timestamp } from 'firebase/firestore';
export interface TuneDataDetails { export interface TuneDataDetails {
readme?: string | null; readme?: string | null;
make?: string | null; make?: string | null;
@ -16,8 +18,8 @@ export interface TuneDataDetails {
export interface TuneDbData { export interface TuneDbData {
userUid?: string; userUid?: string;
createdAt?: Date; createdAt?: Date | Timestamp | string;
updatedAt?: Date; updatedAt?: Date | Timestamp | string;
isPublished?: boolean; isPublished?: boolean;
isListed?: boolean; isListed?: boolean;
isPublic?: boolean; isPublic?: boolean;

View File

@ -3,11 +3,14 @@ import {
Logs, Logs,
Tune, Tune,
} from '@speedy-tuner/types'; } from '@speedy-tuner/types';
import { TuneDbData } from './dbData';
export interface ConfigState extends Config {} export interface ConfigState extends Config {}
export interface TuneState extends Tune {} export interface TuneState extends Tune {}
export interface TuneDataState extends TuneDbData {}
export interface LogsState extends Logs {} export interface LogsState extends Logs {}
export interface UIState { export interface UIState {
@ -24,6 +27,7 @@ export interface NavigationState {
export interface AppState { export interface AppState {
tune: TuneState; tune: TuneState;
tuneData: TuneDataState;
config: ConfigState; config: ConfigState;
logs: LogsState, logs: LogsState,
ui: UIState; ui: UIState;